@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,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
- }