@rev-net/core-v6 0.0.36 → 0.0.39

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 (101) 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 +60 -65
  9. package/src/REVHiddenTokens.sol +2 -2
  10. package/src/REVLoans.sol +134 -90
  11. package/src/REVOwner.sol +124 -17
  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/ADMINISTRATION.md +0 -73
  16. package/ARCHITECTURE.md +0 -116
  17. package/AUDIT_INSTRUCTIONS.md +0 -90
  18. package/RISKS.md +0 -97
  19. package/SKILLS.md +0 -46
  20. package/STYLE_GUIDE.md +0 -610
  21. package/USER_JOURNEYS.md +0 -195
  22. package/foundry.lock +0 -11
  23. package/slither-ci.config.json +0 -10
  24. package/sphinx.lock +0 -507
  25. package/test/REV.integrations.t.sol +0 -573
  26. package/test/REVAutoIssuanceFuzz.t.sol +0 -328
  27. package/test/REVDeployerRegressions.t.sol +0 -396
  28. package/test/REVInvincibility.t.sol +0 -1371
  29. package/test/REVInvincibilityHandler.sol +0 -387
  30. package/test/REVLifecycle.t.sol +0 -420
  31. package/test/REVLoans.invariants.t.sol +0 -724
  32. package/test/REVLoansAttacks.t.sol +0 -816
  33. package/test/REVLoansFeeRecovery.t.sol +0 -783
  34. package/test/REVLoansFindings.t.sol +0 -711
  35. package/test/REVLoansRegressions.t.sol +0 -364
  36. package/test/REVLoansSourceFeeRecovery.t.sol +0 -517
  37. package/test/REVLoansSourced.t.sol +0 -1839
  38. package/test/REVLoansUnSourced.t.sol +0 -409
  39. package/test/TestAuditFixVerification.t.sol +0 -675
  40. package/test/TestBurnHeldTokens.t.sol +0 -394
  41. package/test/TestCEIPattern.t.sol +0 -508
  42. package/test/TestCashOutCallerValidation.t.sol +0 -452
  43. package/test/TestConversionDocumentation.t.sol +0 -368
  44. package/test/TestCrossCurrencyReclaim.t.sol +0 -610
  45. package/test/TestCrossSourceReallocation.t.sol +0 -361
  46. package/test/TestERC2771MetaTx.t.sol +0 -585
  47. package/test/TestEmptyBuybackSpecs.t.sol +0 -300
  48. package/test/TestFlashLoanSurplus.t.sol +0 -365
  49. package/test/TestHiddenTokens.t.sol +0 -474
  50. package/test/TestHookArrayOOB.t.sol +0 -278
  51. package/test/TestLiquidationBehavior.t.sol +0 -398
  52. package/test/TestLoanSourceRotation.t.sol +0 -553
  53. package/test/TestLoansCashOutDelay.t.sol +0 -493
  54. package/test/TestLongTailEconomics.t.sol +0 -677
  55. package/test/TestLowFindings.t.sol +0 -677
  56. package/test/TestMixedFixes.t.sol +0 -593
  57. package/test/TestPermit2Signatures.t.sol +0 -683
  58. package/test/TestReallocationSandwich.t.sol +0 -412
  59. package/test/TestRevnetRegressions.t.sol +0 -350
  60. package/test/TestSplitWeightAdjustment.t.sol +0 -527
  61. package/test/TestSplitWeightE2E.t.sol +0 -605
  62. package/test/TestSplitWeightFork.t.sol +0 -855
  63. package/test/TestStageTransitionBorrowable.t.sol +0 -301
  64. package/test/TestSwapTerminalPermission.t.sol +0 -262
  65. package/test/TestTerminalEncodingInHash.t.sol +0 -326
  66. package/test/TestUint112Overflow.t.sol +0 -311
  67. package/test/TestZeroAmountLoanGuard.t.sol +0 -378
  68. package/test/TestZeroRepayment.t.sol +0 -354
  69. package/test/audit/CodexCrossChainBuybackRouteMismatch.t.sol +0 -184
  70. package/test/audit/CodexPhantomSurplusTerminal.t.sol +0 -367
  71. package/test/audit/CodexREVOwnerRemoteSurplusCurrencyMismatch.t.sol +0 -142
  72. package/test/audit/LoanIdOverflowGuard.t.sol +0 -523
  73. package/test/audit/NemesisOperatorDelegation.t.sol +0 -356
  74. package/test/audit/SupportsInterfaceTest.t.sol +0 -51
  75. package/test/audit/TestFeeAllowanceLeak.t.sol +0 -197
  76. package/test/audit/TestLoansAndDeployerFixes.t.sol +0 -576
  77. package/test/fork/ForkTestBase.sol +0 -727
  78. package/test/fork/TestAutoIssuanceFork.t.sol +0 -148
  79. package/test/fork/TestCashOutFork.t.sol +0 -253
  80. package/test/fork/TestIssuanceDecayFork.t.sol +0 -158
  81. package/test/fork/TestLoanBorrowFork.t.sol +0 -163
  82. package/test/fork/TestLoanCrossRulesetFork.t.sol +0 -308
  83. package/test/fork/TestLoanERC20Fork.t.sol +0 -465
  84. package/test/fork/TestLoanLiquidationFork.t.sol +0 -135
  85. package/test/fork/TestLoanReallocateFork.t.sol +0 -113
  86. package/test/fork/TestLoanRepayFork.t.sol +0 -188
  87. package/test/fork/TestLoanTransferFork.t.sol +0 -143
  88. package/test/fork/TestPermit2PaymentFork.t.sol +0 -300
  89. package/test/fork/TestSplitWeightFork.t.sol +0 -189
  90. package/test/helpers/MaliciousContracts.sol +0 -247
  91. package/test/helpers/REVEmpty721Config.sol +0 -45
  92. package/test/mock/MockBuybackCashOutRecorder.sol +0 -84
  93. package/test/mock/MockBuybackDataHook.sol +0 -112
  94. package/test/mock/MockBuybackDataHookMintPath.sol +0 -68
  95. package/test/mock/MockSuckerRegistry.sol +0 -17
  96. package/test/regression/TestBurnPermissionRequired.t.sol +0 -294
  97. package/test/regression/TestCashOutBuybackFeeLeak.t.sol +0 -232
  98. package/test/regression/TestCrossRevnetLiquidation.t.sol +0 -255
  99. package/test/regression/TestCumulativeLoanCounter.t.sol +0 -361
  100. package/test/regression/TestLiquidateGapHandling.t.sol +0 -394
  101. package/test/regression/TestZeroPriceFeed.t.sol +0 -422
@@ -1,420 +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
- // import /* {*} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
9
- // forge-lint: disable-next-line(unaliased-plain-import)
10
- import /* {*} from */ "./../src/REVDeployer.sol";
11
- // forge-lint: disable-next-line(unaliased-plain-import)
12
- import "@croptop/core-v6/src/CTPublisher.sol";
13
- import {MockBuybackDataHook} from "./mock/MockBuybackDataHook.sol";
14
-
15
- // forge-lint: disable-next-line(unaliased-plain-import)
16
- import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
17
- // forge-lint: disable-next-line(unaliased-plain-import)
18
- import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
19
- // forge-lint: disable-next-line(unaliased-plain-import)
20
- import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
21
- // forge-lint: disable-next-line(unaliased-plain-import)
22
- import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
23
- // forge-lint: disable-next-line(unaliased-plain-import)
24
- import "@bananapus/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
25
-
26
- import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
27
- import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
28
- import {REVLoans} from "../src/REVLoans.sol";
29
- import {REVStageConfig, REVAutoIssuance} from "../src/structs/REVStageConfig.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 {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
42
- import {REVOwner} from "../src/REVOwner.sol";
43
- import {IREVDeployer} from "../src/interfaces/IREVDeployer.sol";
44
- import {MockSuckerRegistry} from "./mock/MockSuckerRegistry.sol";
45
-
46
- /// @notice Full revnet lifecycle E2E: deploy 3-stage -> pay -> advance stages -> cash out.
47
- contract REVLifecycle_Local is TestBaseWorkflow {
48
- // forge-lint: disable-next-line(mixed-case-variable)
49
- bytes32 REV_DEPLOYER_SALT = "REVDeployer";
50
- // forge-lint: disable-next-line(mixed-case-variable)
51
- bytes32 ERC20_SALT = "REV_TOKEN";
52
-
53
- // forge-lint: disable-next-line(mixed-case-variable)
54
- REVDeployer REV_DEPLOYER;
55
- // forge-lint: disable-next-line(mixed-case-variable)
56
- REVOwner REV_OWNER;
57
- // forge-lint: disable-next-line(mixed-case-variable)
58
- JB721TiersHook EXAMPLE_HOOK;
59
- // forge-lint: disable-next-line(mixed-case-variable)
60
- IJB721TiersHookDeployer HOOK_DEPLOYER;
61
- // forge-lint: disable-next-line(mixed-case-variable)
62
- IJB721TiersHookStore HOOK_STORE;
63
- // forge-lint: disable-next-line(mixed-case-variable)
64
- IJBAddressRegistry ADDRESS_REGISTRY;
65
- // forge-lint: disable-next-line(mixed-case-variable)
66
- IREVLoans LOANS_CONTRACT;
67
- // forge-lint: disable-next-line(mixed-case-variable)
68
- IJBSuckerRegistry SUCKER_REGISTRY;
69
- // forge-lint: disable-next-line(mixed-case-variable)
70
- CTPublisher PUBLISHER;
71
- // forge-lint: disable-next-line(mixed-case-variable)
72
- MockBuybackDataHook MOCK_BUYBACK;
73
-
74
- // forge-lint: disable-next-line(mixed-case-variable)
75
- uint256 FEE_PROJECT_ID;
76
- // forge-lint: disable-next-line(mixed-case-variable)
77
- uint256 REVNET_ID;
78
-
79
- // forge-lint: disable-next-line(mixed-case-variable)
80
- address USER1 = makeAddr("user1");
81
- // forge-lint: disable-next-line(mixed-case-variable)
82
- address USER2 = makeAddr("user2");
83
-
84
- // forge-lint: disable-next-line(mixed-case-variable)
85
- uint256 DECIMAL_MULTIPLIER = 10 ** 18;
86
-
87
- address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
88
-
89
- function setUp() public override {
90
- super.setUp();
91
-
92
- FEE_PROJECT_ID = jbProjects().createFor(multisig());
93
-
94
- SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
95
- HOOK_STORE = new JB721TiersHookStore();
96
- EXAMPLE_HOOK = new JB721TiersHook(
97
- jbDirectory(),
98
- jbPermissions(),
99
- jbPrices(),
100
- jbRulesets(),
101
- HOOK_STORE,
102
- jbSplits(),
103
- IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
104
- multisig()
105
- );
106
- ADDRESS_REGISTRY = new JBAddressRegistry();
107
- HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
108
- PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
109
- MOCK_BUYBACK = new MockBuybackDataHook();
110
-
111
- LOANS_CONTRACT = new REVLoans({
112
- controller: jbController(),
113
- suckerRegistry: IJBSuckerRegistry(address(new MockSuckerRegistry())),
114
- revId: FEE_PROJECT_ID,
115
- owner: address(this),
116
- permit2: permit2(),
117
- trustedForwarder: TRUSTED_FORWARDER
118
- });
119
-
120
- REV_OWNER = new REVOwner(
121
- IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
122
- jbDirectory(),
123
- FEE_PROJECT_ID,
124
- SUCKER_REGISTRY,
125
- address(LOANS_CONTRACT),
126
- address(0)
127
- );
128
-
129
- REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
130
- jbController(),
131
- SUCKER_REGISTRY,
132
- FEE_PROJECT_ID,
133
- HOOK_DEPLOYER,
134
- PUBLISHER,
135
- IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
136
- address(LOANS_CONTRACT),
137
- TRUSTED_FORWARDER,
138
- address(REV_OWNER)
139
- );
140
-
141
- REV_OWNER.setDeployer(REV_DEPLOYER);
142
-
143
- vm.prank(multisig());
144
- jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
145
-
146
- // Deploy a 3-stage revnet
147
- _deployThreeStageRevnet();
148
-
149
- // Fund users
150
- vm.deal(USER1, 100e18);
151
- vm.deal(USER2, 100e18);
152
- }
153
-
154
- function _deployThreeStageRevnet() internal {
155
- JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](1);
156
- accountingContextsToAccept[0] = JBAccountingContext({
157
- token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
158
- });
159
-
160
- JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
161
- terminalConfigurations[0] =
162
- JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
163
-
164
- JBSplit[] memory splits = new JBSplit[](1);
165
- splits[0].beneficiary = payable(multisig());
166
- splits[0].percent = 10_000;
167
-
168
- REVStageConfig[] memory stageConfigurations = new REVStageConfig[](3);
169
-
170
- // Stage 0: High issuance, moderate cash out tax
171
- stageConfigurations[0] = REVStageConfig({
172
- startsAtOrAfter: uint40(block.timestamp),
173
- autoIssuances: new REVAutoIssuance[](0),
174
- splitPercent: 0,
175
- splits: splits,
176
- // forge-lint: disable-next-line(unsafe-typecast)
177
- initialIssuance: uint112(1000 * DECIMAL_MULTIPLIER),
178
- issuanceCutFrequency: 90 days,
179
- issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
180
- cashOutTaxRate: 5000, // 50%
181
- extraMetadata: 0
182
- });
183
-
184
- // Stage 1: Lower issuance (inherited with cut)
185
- stageConfigurations[1] = REVStageConfig({
186
- startsAtOrAfter: uint40(block.timestamp + 365 days),
187
- autoIssuances: new REVAutoIssuance[](0),
188
- splitPercent: 0,
189
- splits: splits,
190
- initialIssuance: 0, // inherit
191
- issuanceCutFrequency: 180 days,
192
- issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
193
- cashOutTaxRate: 3000, // 30%
194
- extraMetadata: 0
195
- });
196
-
197
- // Stage 2: Final stage
198
- stageConfigurations[2] = REVStageConfig({
199
- startsAtOrAfter: uint40(block.timestamp + (2 * 365 days)),
200
- autoIssuances: new REVAutoIssuance[](0),
201
- splitPercent: 0,
202
- splits: splits,
203
- initialIssuance: 1,
204
- issuanceCutFrequency: 0,
205
- issuanceCutPercent: 0,
206
- cashOutTaxRate: 500, // 5%
207
- extraMetadata: 0
208
- });
209
-
210
- REVConfig memory revnetConfiguration = REVConfig({
211
- // forge-lint: disable-next-line(named-struct-fields)
212
- description: REVDescription("Lifecycle", "LIFE", "ipfs://lifecycle", "LIFE_TOKEN"),
213
- baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
214
- splitOperator: multisig(),
215
- stageConfigurations: stageConfigurations
216
- });
217
-
218
- vm.prank(multisig());
219
- (REVNET_ID,) = REV_DEPLOYER.deployFor({
220
- revnetId: FEE_PROJECT_ID,
221
- configuration: revnetConfiguration,
222
- terminalConfigurations: terminalConfigurations,
223
- suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
224
- deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("LIFECYCLE_TEST")
225
- }),
226
- tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
227
- allowedPosts: REVEmpty721Config.emptyAllowedPosts()
228
- });
229
- }
230
-
231
- //*********************************************************************//
232
- // --- Lifecycle Tests ---------------------------------------------- //
233
- //*********************************************************************//
234
-
235
- /// @notice Full lifecycle: deploy -> pay in stage 0 -> warp to stage 1 -> pay -> cash out
236
- function test_fullLifecycle_threeStages() public {
237
- // Stage 0: User1 pays 5 ETH
238
- vm.prank(USER1);
239
- uint256 tokens1 =
240
- jbMultiTerminal().pay{value: 5e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 5e18, USER1, 0, "", "");
241
- assertGt(tokens1, 0, "user1 should receive tokens in stage 0");
242
-
243
- // Check total supply after first payment
244
- uint256 totalSupply1 = jbTokens().totalSupplyOf(REVNET_ID);
245
- assertGt(totalSupply1, 0, "total supply should be > 0");
246
-
247
- // Warp to stage 1 (1 year later)
248
- vm.warp(block.timestamp + 365 days);
249
-
250
- // Stage 1: User2 pays 5 ETH (should get fewer tokens due to weight decay)
251
- vm.prank(USER2);
252
- uint256 tokens2 =
253
- jbMultiTerminal().pay{value: 5e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 5e18, USER2, 0, "", "");
254
- assertGt(tokens2, 0, "user2 should receive tokens in stage 1");
255
-
256
- // Total supply increased
257
- uint256 totalSupply2 = jbTokens().totalSupplyOf(REVNET_ID);
258
- assertGt(totalSupply2, totalSupply1, "total supply should increase");
259
-
260
- // Warp to stage 2 (2 years from start)
261
- vm.warp(block.timestamp + 365 days);
262
-
263
- // Stage 2: User1 cashes out some tokens
264
- uint256 cashOutAmount = tokens1 / 2;
265
- vm.prank(USER1);
266
- uint256 reclaimed = jbMultiTerminal()
267
- .cashOutTokensOf({
268
- holder: USER1,
269
- projectId: REVNET_ID,
270
- cashOutCount: cashOutAmount,
271
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
272
- minTokensReclaimed: 0,
273
- beneficiary: payable(USER1),
274
- metadata: ""
275
- });
276
- assertGt(reclaimed, 0, "should reclaim some ETH");
277
-
278
- // Total supply should decrease after cash out
279
- uint256 totalSupply3 = jbTokens().totalSupplyOf(REVNET_ID);
280
- assertLt(totalSupply3, totalSupply2, "total supply should decrease after cash out");
281
- }
282
-
283
- /// @notice Payment in stage 0 gives more tokens than equivalent payment in stage 1.
284
- function test_stageDecay_fewerTokensLater() public {
285
- // Stage 0: Pay 1 ETH
286
- vm.prank(USER1);
287
- uint256 tokensStage0 =
288
- jbMultiTerminal().pay{value: 1e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 1e18, USER1, 0, "", "");
289
-
290
- // Warp to stage 1
291
- vm.warp(block.timestamp + 365 days);
292
-
293
- // Stage 1: Pay 1 ETH
294
- vm.prank(USER2);
295
- uint256 tokensStage1 =
296
- jbMultiTerminal().pay{value: 1e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 1e18, USER2, 0, "", "");
297
-
298
- // User1 should have received more tokens (earlier stage = higher issuance)
299
- assertGt(tokensStage0, tokensStage1, "stage 0 payment should yield more tokens than stage 1");
300
- }
301
-
302
- /// @notice Cash out tax rate differs between stages.
303
- function test_cashOutTax_changesBetweenStages() public {
304
- // Pay in stage 0
305
- vm.prank(USER1);
306
- uint256 tokens =
307
- jbMultiTerminal().pay{value: 10e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 10e18, USER1, 0, "", "");
308
-
309
- // Cash out half in stage 0 (50% tax)
310
- uint256 halfTokens = tokens / 2;
311
- vm.prank(USER1);
312
- uint256 reclaimedStage0 = jbMultiTerminal()
313
- .cashOutTokensOf({
314
- holder: USER1,
315
- projectId: REVNET_ID,
316
- cashOutCount: halfTokens,
317
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
318
- minTokensReclaimed: 0,
319
- beneficiary: payable(USER1),
320
- metadata: ""
321
- });
322
- assertGt(reclaimedStage0, 0, "should reclaim in stage 0");
323
-
324
- // Cash out tax with 50% rate means you get less than proportional share
325
- // (for the only holder with all tokens cashing out half, bonding curve applies)
326
- assertLt(reclaimedStage0, 10e18 / 2, "50% tax should reduce reclaim below proportional share");
327
- }
328
-
329
- /// @notice Terminal balance conservation across lifecycle.
330
- function test_balanceConservation() public {
331
- uint256 payAmount = 10e18;
332
-
333
- // Pay into revnet
334
- vm.prank(USER1);
335
- uint256 tokens =
336
- jbMultiTerminal().pay{value: payAmount}(REVNET_ID, JBConstants.NATIVE_TOKEN, payAmount, USER1, 0, "", "");
337
-
338
- // Record balance
339
- uint256 terminalBalance =
340
- jbTerminalStore().balanceOf(address(jbMultiTerminal()), REVNET_ID, JBConstants.NATIVE_TOKEN);
341
- assertEq(terminalBalance, payAmount, "balance should equal payment");
342
-
343
- // Cash out all tokens
344
- vm.prank(USER1);
345
- uint256 reclaimed = jbMultiTerminal()
346
- .cashOutTokensOf({
347
- holder: USER1,
348
- projectId: REVNET_ID,
349
- cashOutCount: tokens,
350
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
351
- minTokensReclaimed: 0,
352
- beneficiary: payable(USER1),
353
- metadata: ""
354
- });
355
-
356
- // With 50% cash out tax and single holder, reclaiming full supply
357
- // should return less than full amount (due to tax)
358
- assertGt(reclaimed, 0, "should reclaim something");
359
- assertLe(reclaimed, payAmount, "should not reclaim more than paid");
360
-
361
- // Remaining balance should account for what was reclaimed
362
- uint256 terminalBalanceAfter =
363
- jbTerminalStore().balanceOf(address(jbMultiTerminal()), REVNET_ID, JBConstants.NATIVE_TOKEN);
364
- assertEq(
365
- terminalBalanceAfter + reclaimed, terminalBalance, "balance conservation: remaining + reclaimed = original"
366
- );
367
- }
368
-
369
- /// @notice Multiple payers, early payer has advantage.
370
- function test_earlyPayerAdvantage() public {
371
- // User1 pays first
372
- vm.prank(USER1);
373
- uint256 tokens1 =
374
- jbMultiTerminal().pay{value: 5e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 5e18, USER1, 0, "", "");
375
-
376
- // User2 pays same amount later
377
- vm.prank(USER2);
378
- uint256 tokens2 =
379
- jbMultiTerminal().pay{value: 5e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 5e18, USER2, 0, "", "");
380
-
381
- // Both should receive same tokens (same stage, no decay within a cycle without issuanceCutFrequency)
382
- assertEq(tokens1, tokens2, "same amount in same stage should yield same tokens");
383
-
384
- // But when user1 cashes out, they get a smaller share because the surplus grew
385
- // relative to their proportion of the total supply
386
- vm.prank(USER1);
387
- uint256 reclaimed1 = jbMultiTerminal()
388
- .cashOutTokensOf({
389
- holder: USER1,
390
- projectId: REVNET_ID,
391
- cashOutCount: tokens1,
392
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
393
- minTokensReclaimed: 0,
394
- beneficiary: payable(USER1),
395
- metadata: ""
396
- });
397
-
398
- // Should reclaim proportional share (minus tax)
399
- assertGt(reclaimed1, 0, "user1 should reclaim some ETH");
400
- assertLt(reclaimed1, 5e18, "with tax, user1 gets less than they paid");
401
- }
402
-
403
- /// @notice Ruleset IDs match stage indices.
404
- function test_rulesetProgression() public {
405
- // Check stage 0 ruleset
406
- JBRuleset memory ruleset0 = jbRulesets().currentOf(REVNET_ID);
407
- assertGt(ruleset0.id, 0, "should have a valid ruleset");
408
- assertEq(ruleset0.cycleNumber, 1, "first ruleset cycle should be 1");
409
-
410
- // Warp to stage 1
411
- vm.warp(block.timestamp + 365 days);
412
- JBRuleset memory ruleset1 = jbRulesets().currentOf(REVNET_ID);
413
- assertGe(ruleset1.cycleNumber, 1, "cycle should be >= 1");
414
-
415
- // Warp to stage 2
416
- vm.warp(block.timestamp + 365 days);
417
- JBRuleset memory ruleset2 = jbRulesets().currentOf(REVNET_ID);
418
- assertGe(ruleset2.cycleNumber, 1, "cycle should be >= 1");
419
- }
420
- }