@rev-net/core-v6 0.0.37 → 0.0.40

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 (112) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/README.md +6 -7
  3. package/foundry.toml +1 -1
  4. package/package.json +23 -16
  5. package/references/operations.md +1 -1
  6. package/references/runtime.md +1 -1
  7. package/script/Deploy.s.sol +12 -9
  8. package/src/REVDeployer.sol +69 -67
  9. package/src/REVHiddenTokens.sol +2 -2
  10. package/src/REVLoans.sol +26 -22
  11. package/src/REVOwner.sol +147 -29
  12. package/src/interfaces/IREVDeployer.sol +2 -1
  13. package/src/interfaces/IREVHiddenTokens.sol +4 -1
  14. package/src/interfaces/IREVOwner.sol +5 -0
  15. package/src/structs/REVAutoIssuance.sol +4 -2
  16. package/src/structs/REVConfig.sol +8 -5
  17. package/src/structs/REVDescription.sol +6 -5
  18. package/src/structs/REVLoan.sol +8 -5
  19. package/src/structs/REVStageConfig.sol +14 -16
  20. package/ADMINISTRATION.md +0 -73
  21. package/ARCHITECTURE.md +0 -116
  22. package/AUDIT_INSTRUCTIONS.md +0 -90
  23. package/RISKS.md +0 -107
  24. package/SKILLS.md +0 -46
  25. package/STYLE_GUIDE.md +0 -610
  26. package/USER_JOURNEYS.md +0 -195
  27. package/foundry.lock +0 -11
  28. package/slither-ci.config.json +0 -10
  29. package/sphinx.lock +0 -507
  30. package/test/REV.integrations.t.sol +0 -573
  31. package/test/REVAutoIssuanceFuzz.t.sol +0 -328
  32. package/test/REVDeployerRegressions.t.sol +0 -396
  33. package/test/REVInvincibility.t.sol +0 -1371
  34. package/test/REVInvincibilityHandler.sol +0 -387
  35. package/test/REVLifecycle.t.sol +0 -420
  36. package/test/REVLoans.invariants.t.sol +0 -724
  37. package/test/REVLoansAttacks.t.sol +0 -816
  38. package/test/REVLoansFeeRecovery.t.sol +0 -783
  39. package/test/REVLoansFindings.t.sol +0 -711
  40. package/test/REVLoansRegressions.t.sol +0 -364
  41. package/test/REVLoansSourceFeeRecovery.t.sol +0 -517
  42. package/test/REVLoansSourced.t.sol +0 -1839
  43. package/test/REVLoansUnSourced.t.sol +0 -409
  44. package/test/TestAuditFixVerification.t.sol +0 -675
  45. package/test/TestBurnHeldTokens.t.sol +0 -394
  46. package/test/TestCEIPattern.t.sol +0 -508
  47. package/test/TestCashOutCallerValidation.t.sol +0 -452
  48. package/test/TestConversionDocumentation.t.sol +0 -365
  49. package/test/TestCrossCurrencyReclaim.t.sol +0 -610
  50. package/test/TestCrossSourceReallocation.t.sol +0 -361
  51. package/test/TestERC2771MetaTx.t.sol +0 -585
  52. package/test/TestEmptyBuybackSpecs.t.sol +0 -300
  53. package/test/TestFlashLoanSurplus.t.sol +0 -365
  54. package/test/TestHiddenTokens.t.sol +0 -474
  55. package/test/TestHookArrayOOB.t.sol +0 -278
  56. package/test/TestLiquidationBehavior.t.sol +0 -398
  57. package/test/TestLoanSourceRotation.t.sol +0 -553
  58. package/test/TestLoansCashOutDelay.t.sol +0 -493
  59. package/test/TestLongTailEconomics.t.sol +0 -677
  60. package/test/TestLowFindings.t.sol +0 -677
  61. package/test/TestMixedFixes.t.sol +0 -593
  62. package/test/TestPermit2Signatures.t.sol +0 -683
  63. package/test/TestReallocationSandwich.t.sol +0 -412
  64. package/test/TestRevnetRegressions.t.sol +0 -350
  65. package/test/TestSplitWeightAdjustment.t.sol +0 -527
  66. package/test/TestSplitWeightE2E.t.sol +0 -605
  67. package/test/TestSplitWeightFork.t.sol +0 -855
  68. package/test/TestStageTransitionBorrowable.t.sol +0 -301
  69. package/test/TestSwapTerminalPermission.t.sol +0 -262
  70. package/test/TestTerminalEncodingInHash.t.sol +0 -326
  71. package/test/TestUint112Overflow.t.sol +0 -311
  72. package/test/TestZeroAmountLoanGuard.t.sol +0 -378
  73. package/test/TestZeroRepayment.t.sol +0 -354
  74. package/test/audit/CrossChainBuybackRouteMismatch.t.sol +0 -184
  75. package/test/audit/HiddenSupplyCashout.t.sol +0 -61
  76. package/test/audit/LoanIdOverflowGuard.t.sol +0 -523
  77. package/test/audit/NemesisVerification.t.sol +0 -97
  78. package/test/audit/OperatorDelegation.t.sol +0 -356
  79. package/test/audit/PhantomSurplusTerminal.t.sol +0 -367
  80. package/test/audit/REVOwnerCurrencyMismatch.t.sol +0 -188
  81. package/test/audit/REVOwnerRemoteSurplusCurrencyMismatch.t.sol +0 -140
  82. package/test/audit/ReallocatePermission.t.sol +0 -363
  83. package/test/audit/RemoteLoanAccountingGap.t.sol +0 -74
  84. package/test/audit/SupportsInterfaceTest.t.sol +0 -51
  85. package/test/audit/TestFeeAllowanceLeak.t.sol +0 -197
  86. package/test/audit/TestLoansAndDeployerFixes.t.sol +0 -576
  87. package/test/fork/ForkTestBase.sol +0 -727
  88. package/test/fork/TestAutoIssuanceFork.t.sol +0 -148
  89. package/test/fork/TestCashOutFork.t.sol +0 -253
  90. package/test/fork/TestIssuanceDecayFork.t.sol +0 -158
  91. package/test/fork/TestLoanAdversarialFork.t.sol +0 -744
  92. package/test/fork/TestLoanBorrowFork.t.sol +0 -163
  93. package/test/fork/TestLoanCrossRulesetFork.t.sol +0 -308
  94. package/test/fork/TestLoanERC20Fork.t.sol +0 -459
  95. package/test/fork/TestLoanLiquidationFork.t.sol +0 -135
  96. package/test/fork/TestLoanReallocateFork.t.sol +0 -113
  97. package/test/fork/TestLoanRepayFork.t.sol +0 -188
  98. package/test/fork/TestLoanTransferFork.t.sol +0 -143
  99. package/test/fork/TestPermit2PaymentFork.t.sol +0 -300
  100. package/test/fork/TestSplitWeightFork.t.sol +0 -189
  101. package/test/helpers/MaliciousContracts.sol +0 -247
  102. package/test/helpers/REVEmpty721Config.sol +0 -45
  103. package/test/mock/MockBuybackCashOutRecorder.sol +0 -84
  104. package/test/mock/MockBuybackDataHook.sol +0 -112
  105. package/test/mock/MockBuybackDataHookMintPath.sol +0 -68
  106. package/test/mock/MockSuckerRegistry.sol +0 -17
  107. package/test/regression/TestBurnPermissionRequired.t.sol +0 -294
  108. package/test/regression/TestCashOutBuybackFeeLeak.t.sol +0 -232
  109. package/test/regression/TestCrossRevnetLiquidation.t.sol +0 -255
  110. package/test/regression/TestCumulativeLoanCounter.t.sol +0 -361
  111. package/test/regression/TestLiquidateGapHandling.t.sol +0 -394
  112. package/test/regression/TestZeroPriceFeed.t.sol +0 -422
@@ -1,727 +0,0 @@
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
- // forge-lint: disable-next-line(unaliased-plain-import)
13
- import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
14
- // forge-lint: disable-next-line(unaliased-plain-import)
15
- import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
16
- // forge-lint: disable-next-line(unaliased-plain-import)
17
- import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
18
- // forge-lint: disable-next-line(unaliased-plain-import)
19
- import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
20
- // forge-lint: disable-next-line(unaliased-plain-import)
21
- import "@bananapus/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
22
-
23
- import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
24
- import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
25
- import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
26
- import {REVLoans} from "../../src/REVLoans.sol";
27
- import {REVLoan} from "../../src/structs/REVLoan.sol";
28
- import {REVStageConfig, REVAutoIssuance} from "../../src/structs/REVStageConfig.sol";
29
- import {REVLoanSource} from "../../src/structs/REVLoanSource.sol";
30
- import {REVDescription} from "../../src/structs/REVDescription.sol";
31
- import {IREVLoans} from "../../src/interfaces/IREVLoans.sol";
32
- import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
33
- import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
34
- import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
35
- import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
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";
39
- import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
40
- import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
41
- import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
42
- import {IJBBuybackHookRegistry} from "@bananapus/buyback-hook-v6/src/interfaces/IJBBuybackHookRegistry.sol";
43
- // forge-lint: disable-next-line(unused-import)
44
- import {IJBPayHook} from "@bananapus/core-v6/src/interfaces/IJBPayHook.sol";
45
- import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
46
- import {JB721TierConfigFlags} from "@bananapus/721-hook-v6/src/structs/JB721TierConfigFlags.sol";
47
- import {JB721InitTiersConfig} from "@bananapus/721-hook-v6/src/structs/JB721InitTiersConfig.sol";
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";
51
- import {REVDeploy721TiersHookConfig} from "../../src/structs/REVDeploy721TiersHookConfig.sol";
52
- import {REVBaseline721HookConfig} from "../../src/structs/REVBaseline721HookConfig.sol";
53
- import {REV721TiersHookFlags} from "../../src/structs/REV721TiersHookFlags.sol";
54
- import {REVCroptopAllowedPost} from "../../src/structs/REVCroptopAllowedPost.sol";
55
- import {REVEmpty721Config} from "../helpers/REVEmpty721Config.sol";
56
-
57
- // Buyback hook
58
- import {JBBuybackHook} from "@bananapus/buyback-hook-v6/src/JBBuybackHook.sol";
59
- import {JBBuybackHookRegistry} from "@bananapus/buyback-hook-v6/src/JBBuybackHookRegistry.sol";
60
- // forge-lint: disable-next-line(unused-import)
61
- import {IJBBuybackHook} from "@bananapus/buyback-hook-v6/src/interfaces/IJBBuybackHook.sol";
62
- import {IGeomeanOracle} from "@bananapus/buyback-hook-v6/src/interfaces/IGeomeanOracle.sol";
63
-
64
- // Uniswap V4
65
- import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
66
- import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
67
- import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
68
- import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
69
- // forge-lint: disable-next-line(unused-import)
70
- import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
71
- import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
72
- import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
73
- import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
74
- // forge-lint: disable-next-line(unused-import)
75
- import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
76
- import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
77
-
78
- import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
79
- import {REVOwner} from "../../src/REVOwner.sol";
80
- import {IREVDeployer} from "../../src/interfaces/IREVDeployer.sol";
81
- import {MockSuckerRegistry} from "../mock/MockSuckerRegistry.sol";
82
-
83
- /// @notice Helper that adds liquidity to a V4 pool via the unlock/callback pattern.
84
- contract LiquidityHelper is IUnlockCallback {
85
- // forge-lint: disable-next-line(screaming-snake-case-immutable)
86
- IPoolManager public immutable poolManager;
87
-
88
- enum Action {
89
- ADD_LIQUIDITY,
90
- SWAP
91
- }
92
-
93
- struct AddLiqParams {
94
- PoolKey key;
95
- int24 tickLower;
96
- int24 tickUpper;
97
- int256 liquidityDelta;
98
- }
99
-
100
- struct DoSwapParams {
101
- PoolKey key;
102
- bool zeroForOne;
103
- int256 amountSpecified;
104
- uint160 sqrtPriceLimitX96;
105
- }
106
-
107
- constructor(IPoolManager _poolManager) {
108
- poolManager = _poolManager;
109
- }
110
-
111
- function addLiquidity(
112
- PoolKey calldata key,
113
- int24 tickLower,
114
- int24 tickUpper,
115
- int256 liquidityDelta
116
- )
117
- external
118
- payable
119
- {
120
- bytes memory data =
121
- // forge-lint: disable-next-line(named-struct-fields)
122
- abi.encode(Action.ADD_LIQUIDITY, abi.encode(AddLiqParams(key, tickLower, tickUpper, liquidityDelta)));
123
- poolManager.unlock(data);
124
- }
125
-
126
- function swap(
127
- PoolKey calldata key,
128
- bool zeroForOne,
129
- int256 amountSpecified,
130
- uint160 sqrtPriceLimitX96
131
- )
132
- external
133
- payable
134
- {
135
- bytes memory data =
136
- // forge-lint: disable-next-line(named-struct-fields)
137
- abi.encode(Action.SWAP, abi.encode(DoSwapParams(key, zeroForOne, amountSpecified, sqrtPriceLimitX96)));
138
- poolManager.unlock(data);
139
- }
140
-
141
- function unlockCallback(bytes calldata data) external override returns (bytes memory) {
142
- require(msg.sender == address(poolManager), "only PM");
143
-
144
- (Action action, bytes memory inner) = abi.decode(data, (Action, bytes));
145
-
146
- if (action == Action.ADD_LIQUIDITY) {
147
- return _handleAddLiquidity(inner);
148
- } else {
149
- return _handleSwap(inner);
150
- }
151
- }
152
-
153
- function _handleAddLiquidity(bytes memory data) internal returns (bytes memory) {
154
- AddLiqParams memory params = abi.decode(data, (AddLiqParams));
155
-
156
- (BalanceDelta callerDelta,) = poolManager.modifyLiquidity(
157
- params.key,
158
- ModifyLiquidityParams({
159
- tickLower: params.tickLower,
160
- tickUpper: params.tickUpper,
161
- liquidityDelta: params.liquidityDelta,
162
- salt: bytes32(0)
163
- }),
164
- ""
165
- );
166
-
167
- _settleIfNegative(params.key.currency0, callerDelta.amount0());
168
- _settleIfNegative(params.key.currency1, callerDelta.amount1());
169
- _takeIfPositive(params.key.currency0, callerDelta.amount0());
170
- _takeIfPositive(params.key.currency1, callerDelta.amount1());
171
-
172
- return abi.encode(callerDelta);
173
- }
174
-
175
- function _handleSwap(bytes memory data) internal returns (bytes memory) {
176
- DoSwapParams memory params = abi.decode(data, (DoSwapParams));
177
-
178
- BalanceDelta delta = poolManager.swap(
179
- params.key,
180
- // forge-lint: disable-next-line(named-struct-fields)
181
- SwapParams(params.zeroForOne, params.amountSpecified, params.sqrtPriceLimitX96),
182
- ""
183
- );
184
-
185
- if (delta.amount0() < 0) {
186
- _settleIfNegative(params.key.currency0, delta.amount0());
187
- } else {
188
- _takeIfPositive(params.key.currency0, delta.amount0());
189
- }
190
- if (delta.amount1() < 0) {
191
- _settleIfNegative(params.key.currency1, delta.amount1());
192
- } else {
193
- _takeIfPositive(params.key.currency1, delta.amount1());
194
- }
195
-
196
- return abi.encode(delta);
197
- }
198
-
199
- function _settleIfNegative(Currency currency, int128 delta) internal {
200
- if (delta >= 0) return;
201
- // forge-lint: disable-next-line(unsafe-typecast)
202
- uint256 amount = uint256(uint128(-delta));
203
-
204
- if (currency.isAddressZero()) {
205
- poolManager.settle{value: amount}();
206
- } else {
207
- poolManager.sync(currency);
208
- // forge-lint: disable-next-line(erc20-unchecked-transfer)
209
- IERC20(Currency.unwrap(currency)).transfer(address(poolManager), amount);
210
- poolManager.settle();
211
- }
212
- }
213
-
214
- function _takeIfPositive(Currency currency, int128 delta) internal {
215
- if (delta <= 0) return;
216
- // forge-lint: disable-next-line(unsafe-typecast)
217
- uint256 amount = uint256(uint128(delta));
218
- poolManager.take(currency, address(this), amount);
219
- }
220
-
221
- receive() external payable {}
222
- }
223
-
224
- /// @notice Shared base for fork tests. Deploys full JB core + REVDeployer infrastructure on a mainnet fork.
225
- ///
226
- /// Requires: RPC_ETHEREUM_MAINNET env var for mainnet fork (real PoolManager).
227
- abstract contract ForkTestBase is TestBaseWorkflow {
228
- using JBMetadataResolver for bytes;
229
- using PoolIdLibrary for PoolKey;
230
- using CurrencyLibrary for Currency;
231
- using StateLibrary for IPoolManager;
232
-
233
- // ───────────────────────── Mainnet constants
234
- // ─────────────────────────
235
-
236
- address constant POOL_MANAGER_ADDR = 0x000000000004444c5dc75cB358380D2e3dE08A90;
237
-
238
- /// @notice Full-range tick bounds for tickSpacing = 60.
239
- int24 constant TICK_LOWER = -887_200;
240
- int24 constant TICK_UPPER = 887_200;
241
-
242
- // ───────────────────────── State
243
- // ─────────────────────────
244
-
245
- // forge-lint: disable-next-line(mixed-case-variable)
246
- REVDeployer REV_DEPLOYER;
247
- // forge-lint: disable-next-line(mixed-case-variable)
248
- REVOwner REV_OWNER;
249
- // forge-lint: disable-next-line(mixed-case-variable)
250
- JBBuybackHook BUYBACK_HOOK;
251
- // forge-lint: disable-next-line(mixed-case-variable)
252
- JBBuybackHookRegistry BUYBACK_REGISTRY;
253
- // forge-lint: disable-next-line(mixed-case-variable)
254
- JB721TiersHook EXAMPLE_HOOK;
255
- // forge-lint: disable-next-line(mixed-case-variable)
256
- IJB721TiersHookDeployer HOOK_DEPLOYER;
257
- // forge-lint: disable-next-line(mixed-case-variable)
258
- IJB721TiersHookStore HOOK_STORE;
259
- // forge-lint: disable-next-line(mixed-case-variable)
260
- IJBAddressRegistry ADDRESS_REGISTRY;
261
- // forge-lint: disable-next-line(mixed-case-variable)
262
- IREVLoans LOANS_CONTRACT;
263
- // forge-lint: disable-next-line(mixed-case-variable)
264
- IJBSuckerRegistry SUCKER_REGISTRY;
265
- // forge-lint: disable-next-line(mixed-case-variable)
266
- CTPublisher PUBLISHER;
267
- IPoolManager poolManager;
268
- LiquidityHelper liqHelper;
269
-
270
- // forge-lint: disable-next-line(mixed-case-variable)
271
- uint256 FEE_PROJECT_ID;
272
-
273
- address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
274
- // forge-lint: disable-next-line(mixed-case-variable)
275
- address PAYER = makeAddr("payer");
276
- // forge-lint: disable-next-line(mixed-case-variable)
277
- address BORROWER = makeAddr("borrower");
278
- // forge-lint: disable-next-line(mixed-case-variable)
279
- address SPLIT_BENEFICIARY = makeAddr("splitBeneficiary");
280
-
281
- // Tier configuration: 1 ETH tier with 30% split.
282
- uint104 constant TIER_PRICE = 1 ether;
283
- uint32 constant SPLIT_PERCENT = 300_000_000; // 30% of SPLITS_TOTAL_PERCENT (1_000_000_000)
284
- uint112 constant INITIAL_ISSUANCE = 1000e18; // 1000 tokens per ETH
285
-
286
- // ───────────────────────── Setup
287
- // ─────────────────────────
288
-
289
- function setUp() public virtual override {
290
- // Fork mainnet at a stable block — deterministic and post-V4 deployment.
291
- vm.createSelectFork("ethereum", 21_700_000);
292
-
293
- // Verify V4 PoolManager is deployed.
294
- require(POOL_MANAGER_ADDR.code.length > 0, "PoolManager not deployed at expected address");
295
-
296
- // Deploy fresh JB core on the forked mainnet.
297
- super.setUp();
298
-
299
- poolManager = IPoolManager(POOL_MANAGER_ADDR);
300
- liqHelper = new LiquidityHelper(poolManager);
301
-
302
- FEE_PROJECT_ID = jbProjects().createFor(multisig());
303
-
304
- SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
305
- HOOK_STORE = new JB721TiersHookStore();
306
- EXAMPLE_HOOK = new JB721TiersHook(
307
- jbDirectory(),
308
- jbPermissions(),
309
- jbPrices(),
310
- jbRulesets(),
311
- HOOK_STORE,
312
- jbSplits(),
313
- IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
314
- multisig()
315
- );
316
- ADDRESS_REGISTRY = new JBAddressRegistry();
317
- HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
318
- PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
319
-
320
- // Deploy REAL buyback hook with real PoolManager.
321
- BUYBACK_HOOK = new JBBuybackHook(
322
- jbDirectory(),
323
- jbPermissions(),
324
- jbPrices(),
325
- jbProjects(),
326
- jbTokens(),
327
- poolManager,
328
- IHooks(address(0)), // oracleHook
329
- address(0) // trustedForwarder
330
- );
331
-
332
- // Deploy the registry and set the buyback hook as the default.
333
- BUYBACK_REGISTRY = new JBBuybackHookRegistry(
334
- jbPermissions(),
335
- jbProjects(),
336
- address(this), // owner
337
- address(0) // trustedForwarder
338
- );
339
- BUYBACK_REGISTRY.setDefaultHook(IJBRulesetDataHook(address(BUYBACK_HOOK)));
340
-
341
- LOANS_CONTRACT = new REVLoans({
342
- controller: jbController(),
343
- suckerRegistry: IJBSuckerRegistry(address(new MockSuckerRegistry())),
344
- revId: FEE_PROJECT_ID,
345
- owner: address(this),
346
- permit2: permit2(),
347
- trustedForwarder: TRUSTED_FORWARDER
348
- });
349
-
350
- REV_OWNER = new REVOwner(
351
- IJBBuybackHookRegistry(address(BUYBACK_REGISTRY)),
352
- jbDirectory(),
353
- FEE_PROJECT_ID,
354
- SUCKER_REGISTRY,
355
- address(LOANS_CONTRACT),
356
- address(0)
357
- );
358
-
359
- REV_DEPLOYER = new REVDeployer{salt: "REVDeployer_Fork"}(
360
- jbController(),
361
- SUCKER_REGISTRY,
362
- FEE_PROJECT_ID,
363
- HOOK_DEPLOYER,
364
- PUBLISHER,
365
- IJBBuybackHookRegistry(address(BUYBACK_REGISTRY)),
366
- address(LOANS_CONTRACT),
367
- TRUSTED_FORWARDER,
368
- address(REV_OWNER)
369
- );
370
-
371
- REV_OWNER.setDeployer(REV_DEPLOYER);
372
-
373
- vm.prank(multisig());
374
- jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
375
-
376
- // Fund payer and borrower.
377
- vm.deal(PAYER, 100 ether);
378
- vm.deal(BORROWER, 100 ether);
379
- }
380
-
381
- // ───────────────────────── Config Helpers
382
- // ─────────────────────────
383
-
384
- function _buildMinimalConfig(uint16 cashOutTaxRate)
385
- internal
386
- view
387
- returns (REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc)
388
- {
389
- JBAccountingContext[] memory acc = new JBAccountingContext[](1);
390
- acc[0] = JBAccountingContext({
391
- token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
392
- });
393
- tc = new JBTerminalConfig[](1);
394
- tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
395
-
396
- REVStageConfig[] memory stages = new REVStageConfig[](1);
397
- JBSplit[] memory splits = new JBSplit[](1);
398
- splits[0].beneficiary = payable(multisig());
399
- splits[0].percent = 10_000;
400
- stages[0] = REVStageConfig({
401
- startsAtOrAfter: uint40(block.timestamp),
402
- autoIssuances: new REVAutoIssuance[](0),
403
- splitPercent: 0,
404
- splits: splits,
405
- initialIssuance: INITIAL_ISSUANCE,
406
- issuanceCutFrequency: 0,
407
- issuanceCutPercent: 0,
408
- cashOutTaxRate: cashOutTaxRate,
409
- extraMetadata: 0
410
- });
411
-
412
- cfg = REVConfig({
413
- // forge-lint: disable-next-line(named-struct-fields)
414
- description: REVDescription("Fork Test", "FORK", "ipfs://fork", "FORK_SALT"),
415
- baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
416
- splitOperator: multisig(),
417
- stageConfigurations: stages
418
- });
419
-
420
- sdc = REVSuckerDeploymentConfig({
421
- deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("FORK_TEST"))
422
- });
423
- }
424
-
425
- function _build721Config() internal view returns (REVDeploy721TiersHookConfig memory) {
426
- JB721TierConfig[] memory tiers = new JB721TierConfig[](1);
427
- JBSplit[] memory tierSplits = new JBSplit[](1);
428
- tierSplits[0] = JBSplit({
429
- preferAddToBalance: false,
430
- percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
431
- projectId: 0,
432
- beneficiary: payable(SPLIT_BENEFICIARY),
433
- lockedUntil: 0,
434
- hook: IJBSplitHook(address(0))
435
- });
436
-
437
- tiers[0] = JB721TierConfig({
438
- price: TIER_PRICE,
439
- initialSupply: 100,
440
- votingUnits: 0,
441
- reserveFrequency: 0,
442
- reserveBeneficiary: address(0),
443
- // forge-lint: disable-next-line(unsafe-typecast)
444
- encodedIPFSUri: bytes32("tier1"),
445
- category: 1,
446
- discountPercent: 0,
447
- flags: JB721TierConfigFlags({
448
- allowOwnerMint: false,
449
- useReserveBeneficiaryAsDefault: false,
450
- transfersPausable: false,
451
- useVotingUnits: false,
452
- cantBeRemoved: false,
453
- cantIncreaseDiscountPercent: false,
454
- cantBuyWithCredits: false
455
- }),
456
- splitPercent: SPLIT_PERCENT,
457
- splits: tierSplits
458
- });
459
-
460
- return REVDeploy721TiersHookConfig({
461
- baseline721HookConfiguration: REVBaseline721HookConfig({
462
- name: "Fork NFT",
463
- symbol: "FNFT",
464
- baseUri: "ipfs://",
465
- tokenUriResolver: IJB721TokenUriResolver(address(0)),
466
- contractUri: "ipfs://contract",
467
- tiersConfig: JB721InitTiersConfig({
468
- tiers: tiers, currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18
469
- }),
470
- flags: REV721TiersHookFlags({
471
- noNewTiersWithReserves: false,
472
- noNewTiersWithVotes: false,
473
- noNewTiersWithOwnerMinting: false,
474
- preventOverspending: false
475
- })
476
- }),
477
- // forge-lint: disable-next-line(unsafe-typecast)
478
- salt: bytes32("FORK_721"),
479
- preventSplitOperatorAdjustingTiers: false,
480
- preventSplitOperatorUpdatingMetadata: false,
481
- preventSplitOperatorMinting: false,
482
- preventSplitOperatorIncreasingDiscountPercent: false
483
- });
484
- }
485
-
486
- // ───────────────────────── Deployment Helpers
487
- // ─────────────────────────
488
-
489
- /// @notice Deploy the fee project using the given tax rate.
490
- function _deployFeeProject(uint16 cashOutTaxRate) internal {
491
- (REVConfig memory feeCfg, JBTerminalConfig[] memory feeTc, REVSuckerDeploymentConfig memory feeSdc) =
492
- _buildMinimalConfig(cashOutTaxRate);
493
- // forge-lint: disable-next-line(named-struct-fields)
494
- feeCfg.description = REVDescription("Fee", "FEE", "ipfs://fee", "FEE_SALT");
495
-
496
- vm.prank(multisig());
497
- REV_DEPLOYER.deployFor({
498
- revnetId: FEE_PROJECT_ID,
499
- configuration: feeCfg,
500
- terminalConfigurations: feeTc,
501
- suckerDeploymentConfiguration: feeSdc,
502
- tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
503
- allowedPosts: REVEmpty721Config.emptyAllowedPosts()
504
- });
505
- }
506
-
507
- /// @notice Deploy a revnet (no 721 hook) with the given cash out tax rate.
508
- function _deployRevnet(uint16 cashOutTaxRate) internal returns (uint256 revnetId) {
509
- (REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc) =
510
- _buildMinimalConfig(cashOutTaxRate);
511
-
512
- (revnetId,) = REV_DEPLOYER.deployFor({
513
- revnetId: 0,
514
- configuration: cfg,
515
- terminalConfigurations: tc,
516
- suckerDeploymentConfiguration: sdc,
517
- tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
518
- allowedPosts: REVEmpty721Config.emptyAllowedPosts()
519
- });
520
- }
521
-
522
- /// @notice Deploy a revnet with 721 tiers and the given cash out tax rate.
523
- function _deployRevnetWith721(uint16 cashOutTaxRate) internal returns (uint256 revnetId, IJB721TiersHook hook) {
524
- (REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc) =
525
- _buildMinimalConfig(cashOutTaxRate);
526
- // Use a different salt to avoid CREATE2 collision with _deployRevnet's ERC-20.
527
- cfg.description.salt = "FORK_721_SALT";
528
- REVDeploy721TiersHookConfig memory hookConfig = _build721Config();
529
-
530
- (revnetId, hook) = REV_DEPLOYER.deployFor({
531
- revnetId: 0,
532
- configuration: cfg,
533
- terminalConfigurations: tc,
534
- suckerDeploymentConfiguration: sdc,
535
- tiered721HookConfiguration: hookConfig,
536
- allowedPosts: new REVCroptopAllowedPost[](0)
537
- });
538
- }
539
-
540
- // ───────────────────────── Pool Helpers
541
- // ─────────────────────────
542
-
543
- /// @notice Set up a V4 pool for the revnet's project token / native ETH pair at 1:1 price.
544
- function _setupPool(uint256 revnetId, uint256 liquidityTokenAmount) internal returns (PoolKey memory key) {
545
- address projectToken = address(jbTokens().tokenOf(revnetId));
546
- require(projectToken != address(0), "project token not deployed");
547
-
548
- // Native ETH is represented as address(0) in V4 pool keys.
549
- // address(0) is always less than any deployed token address.
550
- key = PoolKey({
551
- currency0: Currency.wrap(address(0)),
552
- currency1: Currency.wrap(projectToken),
553
- fee: REV_DEPLOYER.DEFAULT_BUYBACK_POOL_FEE(),
554
- tickSpacing: REV_DEPLOYER.DEFAULT_BUYBACK_TICK_SPACING(),
555
- hooks: IHooks(address(0))
556
- });
557
-
558
- // Pool is already initialized at fair issuance price by REVDeployer during deployment.
559
- // At high tick (~69078 for 1000 tokens/ETH), full-range liquidity needs ~32x more project tokens than ETH.
560
- // Mint 50x project tokens and use a smaller liquidity delta to stay within budget.
561
- vm.prank(address(jbController()));
562
- jbTokens().mintFor(address(liqHelper), revnetId, liquidityTokenAmount * 50);
563
- // Fund with ETH for the native currency side.
564
- vm.deal(address(liqHelper), liquidityTokenAmount);
565
-
566
- vm.startPrank(address(liqHelper));
567
- IERC20(projectToken).approve(address(poolManager), type(uint256).max);
568
- vm.stopPrank();
569
-
570
- // forge-lint: disable-next-line(unsafe-typecast)
571
- int256 liquidityDelta = int256(liquidityTokenAmount / 50);
572
- vm.prank(address(liqHelper));
573
- liqHelper.addLiquidity{value: liquidityTokenAmount}(key, TICK_LOWER, TICK_UPPER, liquidityDelta);
574
-
575
- // Mock geomean oracle at tick 69078 (~1000 tokens/ETH, matching INITIAL_ISSUANCE).
576
- _mockOracle(liquidityDelta, 69_078, uint32(REV_DEPLOYER.DEFAULT_BUYBACK_TWAP_WINDOW()));
577
- }
578
-
579
- /// @notice Mock the IGeomeanOracle at address(0) for hookless pools.
580
- function _mockOracle(int256 liquidity, int24 tick, uint32 twapWindow) internal {
581
- vm.etch(address(0), hex"00");
582
-
583
- int56[] memory tickCumulatives = new int56[](2);
584
- tickCumulatives[0] = 0;
585
- // forge-lint: disable-next-line(unsafe-typecast)
586
- tickCumulatives[1] = int56(tick) * int56(int32(twapWindow));
587
-
588
- uint136[] memory secondsPerLiquidityCumulativeX128s = new uint136[](2);
589
- secondsPerLiquidityCumulativeX128s[0] = 0;
590
- uint256 liq = uint256(liquidity > 0 ? liquidity : -liquidity);
591
- if (liq == 0) liq = 1;
592
- // forge-lint: disable-next-line(unsafe-typecast)
593
- secondsPerLiquidityCumulativeX128s[1] = uint136((uint256(twapWindow) << 128) / liq);
594
-
595
- vm.mockCall(
596
- address(0),
597
- abi.encodeWithSelector(IGeomeanOracle.observe.selector),
598
- abi.encode(tickCumulatives, secondsPerLiquidityCumulativeX128s)
599
- );
600
- }
601
-
602
- // ───────────────────────── Balance Helpers
603
- // ─────────────────────────
604
-
605
- /// @notice Get a project's balance in the terminal store.
606
- function _terminalBalance(uint256 projectId, address token) internal view returns (uint256) {
607
- return jbTerminalStore().balanceOf(address(jbMultiTerminal()), projectId, token);
608
- }
609
-
610
- // ───────────────────────── Payment Helpers
611
- // ─────────────────────────
612
-
613
- /// @notice Pay the revnet with ETH.
614
- function _payRevnet(uint256 revnetId, address payer, uint256 amount) internal returns (uint256 tokensReceived) {
615
- vm.prank(payer);
616
- tokensReceived = jbMultiTerminal().pay{value: amount}({
617
- projectId: revnetId,
618
- token: JBConstants.NATIVE_TOKEN,
619
- amount: amount,
620
- beneficiary: payer,
621
- minReturnedTokens: 0,
622
- memo: "",
623
- metadata: ""
624
- });
625
- }
626
-
627
- // ───────────────────────── Metadata Helpers
628
- // ─────────────────────────
629
-
630
- /// @notice Build payment metadata with both 721 tier selection AND buyback quote.
631
- function _buildPayMetadataWithQuote(
632
- address hookMetadataTarget,
633
- uint256 amountToSwapWith,
634
- uint256 minimumSwapAmountOut
635
- )
636
- internal
637
- view
638
- returns (bytes memory)
639
- {
640
- uint16[] memory tierIds = new uint16[](1);
641
- tierIds[0] = 1;
642
- bytes memory tierData = abi.encode(true, tierIds);
643
- bytes4 tierMetadataId = JBMetadataResolver.getId("pay", hookMetadataTarget);
644
-
645
- bytes memory quoteData = abi.encode(amountToSwapWith, minimumSwapAmountOut);
646
- bytes4 quoteMetadataId = JBMetadataResolver.getId("quote");
647
-
648
- bytes4[] memory ids = new bytes4[](2);
649
- ids[0] = tierMetadataId;
650
- ids[1] = quoteMetadataId;
651
- bytes[] memory datas = new bytes[](2);
652
- datas[0] = tierData;
653
- datas[1] = quoteData;
654
-
655
- return JBMetadataResolver.createMetadata(ids, datas);
656
- }
657
-
658
- /// @notice Build payment metadata with only 721 tier selection (no quote -> TWAP/spot fallback).
659
- function _buildPayMetadataNoQuote(address hookMetadataTarget) internal pure returns (bytes memory) {
660
- uint16[] memory tierIds = new uint16[](1);
661
- tierIds[0] = 1;
662
- bytes memory tierData = abi.encode(true, tierIds);
663
- bytes4 tierMetadataId = JBMetadataResolver.getId("pay", hookMetadataTarget);
664
-
665
- bytes4[] memory ids = new bytes4[](1);
666
- ids[0] = tierMetadataId;
667
- bytes[] memory datas = new bytes[](1);
668
- datas[0] = tierData;
669
-
670
- return JBMetadataResolver.createMetadata(ids, datas);
671
- }
672
-
673
- // ───────────────────────── ERC721 Helpers
674
- // ─────────────────────────
675
-
676
- /// @notice Get the owner of a loan NFT. REVLoans is ERC721 but typed as IREVLoans.
677
- function _loanOwnerOf(uint256 loanId) internal view returns (address) {
678
- return REVLoans(payable(address(LOANS_CONTRACT))).ownerOf(loanId);
679
- }
680
-
681
- // ───────────────────────── Loan Helpers
682
- // ─────────────────────────
683
-
684
- /// @notice Build a native token loan source.
685
- function _nativeLoanSource() internal view returns (REVLoanSource memory) {
686
- return REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
687
- }
688
-
689
- /// @notice Grant BURN_TOKENS permission to the loans contract for a given account.
690
- function _grantBurnPermission(address account, uint256 revnetId) internal {
691
- // Permission ID 11 = BURN_TOKENS in JBPermissionIds
692
- mockExpect(
693
- address(jbPermissions()),
694
- abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), account, revnetId, 11, true, true)),
695
- abi.encode(true)
696
- );
697
- }
698
-
699
- /// @notice Create a loan for the given borrower. Returns the loan ID and loan struct.
700
- function _createLoan(
701
- uint256 revnetId,
702
- address borrower,
703
- uint256 collateral,
704
- uint256 prepaidFeePercent
705
- )
706
- internal
707
- returns (uint256 loanId, REVLoan memory loan)
708
- {
709
- REVLoanSource memory source = _nativeLoanSource();
710
- uint256 borrowable =
711
- LOANS_CONTRACT.borrowableAmountFrom(revnetId, collateral, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
712
- require(borrowable > 0, "no borrowable amount");
713
-
714
- _grantBurnPermission(borrower, revnetId);
715
-
716
- vm.prank(borrower);
717
- (loanId, loan) = LOANS_CONTRACT.borrowFrom({
718
- revnetId: revnetId,
719
- source: source,
720
- minBorrowAmount: 0,
721
- collateralCount: collateral,
722
- beneficiary: payable(borrower),
723
- prepaidFeePercent: prepaidFeePercent,
724
- holder: borrower
725
- });
726
- }
727
- }