@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,677 +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
- import {MockBuybackDataHook} from "./mock/MockBuybackDataHook.sol";
13
- // forge-lint: disable-next-line(unaliased-plain-import)
14
- import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
15
- // forge-lint: disable-next-line(unaliased-plain-import)
16
- import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
17
- // forge-lint: disable-next-line(unaliased-plain-import)
18
- import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
19
- // forge-lint: disable-next-line(unaliased-plain-import)
20
- import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
21
- // forge-lint: disable-next-line(unaliased-plain-import)
22
- import "@bananapus/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
23
-
24
- import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
25
- import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
26
- import {REVLoans} from "../src/REVLoans.sol";
27
- import {REVStageConfig, REVAutoIssuance} from "../src/structs/REVStageConfig.sol";
28
- import {REVDescription} from "../src/structs/REVDescription.sol";
29
- import {IREVLoans} from "./../src/interfaces/IREVLoans.sol";
30
- import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
31
- import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
32
- import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
33
- import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
34
- import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
35
- import {JB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/JB721CheckpointsDeployer.sol";
36
- import {IJB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721CheckpointsDeployer.sol";
37
- import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
38
- import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
39
- import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
40
- import {REVOwner} from "../src/REVOwner.sol";
41
- import {IREVDeployer} from "../src/interfaces/IREVDeployer.sol";
42
- import {MockSuckerRegistry} from "./mock/MockSuckerRegistry.sol";
43
-
44
- /// @notice Long-tail economic simulation: run a revnet through multiple stage transitions with many payments
45
- /// and cash outs, verifying value conservation and bonding curve consistency.
46
- contract TestLongTailEconomics is TestBaseWorkflow {
47
- // forge-lint: disable-next-line(mixed-case-variable)
48
- bytes32 REV_DEPLOYER_SALT = "REVDeployer";
49
-
50
- // forge-lint: disable-next-line(mixed-case-variable)
51
- REVDeployer REV_DEPLOYER;
52
- // forge-lint: disable-next-line(mixed-case-variable)
53
- REVOwner REV_OWNER;
54
- // forge-lint: disable-next-line(mixed-case-variable)
55
- JB721TiersHook EXAMPLE_HOOK;
56
- // forge-lint: disable-next-line(mixed-case-variable)
57
- IJB721TiersHookDeployer HOOK_DEPLOYER;
58
- // forge-lint: disable-next-line(mixed-case-variable)
59
- IJB721TiersHookStore HOOK_STORE;
60
- // forge-lint: disable-next-line(mixed-case-variable)
61
- IJBAddressRegistry ADDRESS_REGISTRY;
62
- // forge-lint: disable-next-line(mixed-case-variable)
63
- IREVLoans LOANS_CONTRACT;
64
- // forge-lint: disable-next-line(mixed-case-variable)
65
- IJBSuckerRegistry SUCKER_REGISTRY;
66
- // forge-lint: disable-next-line(mixed-case-variable)
67
- CTPublisher PUBLISHER;
68
- // forge-lint: disable-next-line(mixed-case-variable)
69
- MockBuybackDataHook MOCK_BUYBACK;
70
-
71
- // forge-lint: disable-next-line(mixed-case-variable)
72
- uint256 FEE_PROJECT_ID;
73
- // forge-lint: disable-next-line(mixed-case-variable)
74
- uint256 REVNET_ID;
75
-
76
- // forge-lint: disable-next-line(mixed-case-variable)
77
- uint256 DECIMAL_MULTIPLIER = 10 ** 18;
78
-
79
- address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
80
-
81
- /// @notice Creates 10 distinct user addresses for the simulation.
82
- function _makeUsers(uint256 count) internal returns (address[] memory users) {
83
- users = new address[](count);
84
- for (uint256 i; i < count; i++) {
85
- users[i] = makeAddr(string(abi.encodePacked("econ_user_", vm.toString(i))));
86
- vm.deal(users[i], 1000e18);
87
- }
88
- }
89
-
90
- function setUp() public override {
91
- super.setUp();
92
-
93
- FEE_PROJECT_ID = jbProjects().createFor(multisig());
94
-
95
- SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
96
- HOOK_STORE = new JB721TiersHookStore();
97
- EXAMPLE_HOOK = new JB721TiersHook(
98
- jbDirectory(),
99
- jbPermissions(),
100
- jbPrices(),
101
- jbRulesets(),
102
- HOOK_STORE,
103
- jbSplits(),
104
- IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
105
- multisig()
106
- );
107
- ADDRESS_REGISTRY = new JBAddressRegistry();
108
- HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
109
- PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
110
- MOCK_BUYBACK = new MockBuybackDataHook();
111
-
112
- LOANS_CONTRACT = new REVLoans({
113
- controller: jbController(),
114
- suckerRegistry: IJBSuckerRegistry(address(new MockSuckerRegistry())),
115
- revId: FEE_PROJECT_ID,
116
- owner: address(this),
117
- permit2: permit2(),
118
- trustedForwarder: TRUSTED_FORWARDER
119
- });
120
-
121
- REV_OWNER = new REVOwner(
122
- IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
123
- jbDirectory(),
124
- FEE_PROJECT_ID,
125
- SUCKER_REGISTRY,
126
- address(LOANS_CONTRACT),
127
- address(0)
128
- );
129
-
130
- REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
131
- jbController(),
132
- SUCKER_REGISTRY,
133
- FEE_PROJECT_ID,
134
- HOOK_DEPLOYER,
135
- PUBLISHER,
136
- IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
137
- address(LOANS_CONTRACT),
138
- TRUSTED_FORWARDER,
139
- address(REV_OWNER)
140
- );
141
-
142
- REV_OWNER.setDeployer(REV_DEPLOYER);
143
-
144
- vm.prank(multisig());
145
- jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
146
-
147
- _deployFeeProject();
148
- _deployThreeStageRevnet();
149
- }
150
-
151
- function _deployFeeProject() internal {
152
- JBAccountingContext[] memory acc = new JBAccountingContext[](1);
153
- acc[0] = JBAccountingContext({
154
- token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
155
- });
156
- JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
157
- tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
158
-
159
- JBSplit[] memory splits = new JBSplit[](1);
160
- splits[0].beneficiary = payable(multisig());
161
- splits[0].percent = 10_000;
162
-
163
- REVStageConfig[] memory stages = new REVStageConfig[](1);
164
- stages[0] = REVStageConfig({
165
- startsAtOrAfter: uint40(block.timestamp),
166
- autoIssuances: new REVAutoIssuance[](0),
167
- splitPercent: 0,
168
- splits: splits,
169
- initialIssuance: uint112(1000 * DECIMAL_MULTIPLIER),
170
- issuanceCutFrequency: 90 days,
171
- issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
172
- cashOutTaxRate: 5000,
173
- extraMetadata: 0
174
- });
175
-
176
- REVConfig memory cfg = REVConfig({
177
- // forge-lint: disable-next-line(named-struct-fields)
178
- description: REVDescription("Revnet", "$REV", "ipfs://fee", "REV_TOKEN"),
179
- baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
180
- splitOperator: multisig(),
181
- stageConfigurations: stages
182
- });
183
-
184
- vm.prank(multisig());
185
- REV_DEPLOYER.deployFor({
186
- revnetId: FEE_PROJECT_ID,
187
- configuration: cfg,
188
- terminalConfigurations: tc,
189
- suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
190
- deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("FEE")
191
- }),
192
- tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
193
- allowedPosts: REVEmpty721Config.emptyAllowedPosts()
194
- });
195
- }
196
-
197
- function _deployThreeStageRevnet() internal {
198
- JBAccountingContext[] memory acc = new JBAccountingContext[](1);
199
- acc[0] = JBAccountingContext({
200
- token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
201
- });
202
- JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
203
- tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
204
-
205
- JBSplit[] memory splits = new JBSplit[](1);
206
- splits[0].beneficiary = payable(multisig());
207
- splits[0].percent = 10_000;
208
-
209
- REVStageConfig[] memory stageConfigurations = new REVStageConfig[](3);
210
-
211
- // Stage 0: High issuance, moderate cash out tax, 90-day cut frequency.
212
- stageConfigurations[0] = REVStageConfig({
213
- startsAtOrAfter: uint40(block.timestamp),
214
- autoIssuances: new REVAutoIssuance[](0),
215
- splitPercent: 1000, // 10% reserved split
216
- splits: splits,
217
- // forge-lint: disable-next-line(unsafe-typecast)
218
- initialIssuance: uint112(1000 * DECIMAL_MULTIPLIER),
219
- issuanceCutFrequency: 90 days,
220
- issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
221
- cashOutTaxRate: 5000, // 50%
222
- extraMetadata: 0
223
- });
224
-
225
- // Stage 1: Lower issuance inherited with cut, 180-day frequency, lower tax.
226
- stageConfigurations[1] = REVStageConfig({
227
- startsAtOrAfter: uint40(block.timestamp + 365 days),
228
- autoIssuances: new REVAutoIssuance[](0),
229
- splitPercent: 500, // 5% reserved split
230
- splits: splits,
231
- initialIssuance: 0, // inherit from previous
232
- issuanceCutFrequency: 180 days,
233
- issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
234
- cashOutTaxRate: 3000, // 30%
235
- extraMetadata: 0
236
- });
237
-
238
- // Stage 2: Terminal stage with minimal issuance.
239
- stageConfigurations[2] = REVStageConfig({
240
- startsAtOrAfter: uint40(block.timestamp + (2 * 365 days)),
241
- autoIssuances: new REVAutoIssuance[](0),
242
- splitPercent: 0,
243
- splits: splits,
244
- initialIssuance: 1, // Near-zero issuance.
245
- issuanceCutFrequency: 0,
246
- issuanceCutPercent: 0,
247
- cashOutTaxRate: 500, // 5%
248
- extraMetadata: 0
249
- });
250
-
251
- REVConfig memory cfg = REVConfig({
252
- // forge-lint: disable-next-line(named-struct-fields)
253
- description: REVDescription("LongTail", "LTAIL", "ipfs://longtail", "LTAIL_TOKEN"),
254
- baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
255
- splitOperator: multisig(),
256
- stageConfigurations: stageConfigurations
257
- });
258
-
259
- (REVNET_ID,) = REV_DEPLOYER.deployFor({
260
- revnetId: 0,
261
- configuration: cfg,
262
- terminalConfigurations: tc,
263
- suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
264
- deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("LONGTAIL")
265
- }),
266
- tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
267
- allowedPosts: REVEmpty721Config.emptyAllowedPosts()
268
- });
269
- }
270
-
271
- //*********************************************************************//
272
- // --- Long-Tail Economics Tests ------------------------------------- //
273
- //*********************************************************************//
274
-
275
- /// @notice Simulate 100+ payments spread across all three stages.
276
- /// Verify that tokens are minted for every payment and that issuance decays over time.
277
- function test_manyPayments_acrossAllStages() public {
278
- address[] memory users = _makeUsers(10);
279
-
280
- // Track tokens minted per stage for comparison.
281
- uint256 totalTokensStage0;
282
- uint256 totalTokensStage1;
283
- uint256 totalTokensStage2;
284
-
285
- // Stage 0: 40 payments over the first year.
286
- for (uint256 i; i < 40; i++) {
287
- address user = users[i % 10];
288
- uint256 payAmount = 0.5e18 + (i * 0.01e18); // Vary amounts slightly.
289
- vm.prank(user);
290
- uint256 tokens = jbMultiTerminal().pay{value: payAmount}(
291
- REVNET_ID, JBConstants.NATIVE_TOKEN, payAmount, user, 0, "", ""
292
- );
293
- assertGt(tokens, 0, "should mint tokens in stage 0");
294
- totalTokensStage0 += tokens;
295
-
296
- // Advance 9 days per payment (360 days total, just under stage 1).
297
- vm.warp(block.timestamp + 9 days);
298
- }
299
-
300
- // Now in stage 1 (365 days from start).
301
- vm.warp(block.timestamp + 5 days); // Ensure we are past the stage 1 start.
302
-
303
- // Stage 1: 40 payments.
304
- for (uint256 i; i < 40; i++) {
305
- address user = users[i % 10];
306
- uint256 payAmount = 0.5e18;
307
- vm.prank(user);
308
- uint256 tokens = jbMultiTerminal().pay{value: payAmount}(
309
- REVNET_ID, JBConstants.NATIVE_TOKEN, payAmount, user, 0, "", ""
310
- );
311
- assertGt(tokens, 0, "should mint tokens in stage 1");
312
- totalTokensStage1 += tokens;
313
-
314
- // Advance 9 days per payment.
315
- vm.warp(block.timestamp + 9 days);
316
- }
317
-
318
- // Now advance to stage 2 (2 years from start).
319
- vm.warp(block.timestamp + 365 days);
320
-
321
- // Stage 2: 30 payments.
322
- for (uint256 i; i < 30; i++) {
323
- address user = users[i % 10];
324
- uint256 payAmount = 0.5e18;
325
- vm.prank(user);
326
- uint256 tokens = jbMultiTerminal().pay{value: payAmount}(
327
- REVNET_ID, JBConstants.NATIVE_TOKEN, payAmount, user, 0, "", ""
328
- );
329
- // Stage 2 has initialIssuance=1 (near zero), so tokens might be very small.
330
- totalTokensStage2 += tokens;
331
-
332
- vm.warp(block.timestamp + 1 days);
333
- }
334
-
335
- // Stage 2 should issue drastically fewer tokens than stage 0.
336
- assertGt(totalTokensStage0, totalTokensStage1, "stage 0 should issue more tokens than stage 1");
337
- // Stage 2 has issuance=1, so its total should be much less.
338
- if (totalTokensStage2 > 0) {
339
- assertGt(totalTokensStage1, totalTokensStage2, "stage 1 should issue more tokens than stage 2");
340
- }
341
- }
342
-
343
- /// @notice Value conservation: total ETH reclaimed by all cash-outs plus remaining terminal balance
344
- /// should equal total ETH paid in, minus fees paid to the fee project.
345
- function test_valueConservation_payAndCashOut() public {
346
- address[] memory users = _makeUsers(5);
347
- uint256 totalPaidIn;
348
-
349
- // Each user pays 10 ETH.
350
- uint256[] memory userTokens = new uint256[](5);
351
- for (uint256 i; i < 5; i++) {
352
- uint256 payAmount = 10e18;
353
- vm.prank(users[i]);
354
- userTokens[i] = jbMultiTerminal().pay{value: payAmount}(
355
- REVNET_ID, JBConstants.NATIVE_TOKEN, payAmount, users[i], 0, "", ""
356
- );
357
- totalPaidIn += payAmount;
358
- }
359
-
360
- // Users 0-2 cash out all their tokens.
361
- uint256 totalReclaimed;
362
- for (uint256 i; i < 3; i++) {
363
- vm.prank(users[i]);
364
- uint256 reclaimed = jbMultiTerminal()
365
- .cashOutTokensOf({
366
- holder: users[i],
367
- projectId: REVNET_ID,
368
- cashOutCount: userTokens[i],
369
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
370
- minTokensReclaimed: 0,
371
- beneficiary: payable(users[i]),
372
- metadata: ""
373
- });
374
- totalReclaimed += reclaimed;
375
- }
376
-
377
- // Remaining terminal balance.
378
- uint256 terminalBalance =
379
- jbTerminalStore().balanceOf(address(jbMultiTerminal()), REVNET_ID, JBConstants.NATIVE_TOKEN);
380
-
381
- // Fee project balance (fees are paid to project 1).
382
- uint256 feeBalance =
383
- jbTerminalStore().balanceOf(address(jbMultiTerminal()), FEE_PROJECT_ID, JBConstants.NATIVE_TOKEN);
384
-
385
- // Conservation: totalPaidIn = totalReclaimed + terminalBalance + feeBalance.
386
- // Allow a small tolerance for rounding (a few wei per operation).
387
- uint256 accountedFor = totalReclaimed + terminalBalance + feeBalance;
388
- assertApproxEqAbs(
389
- accountedFor,
390
- totalPaidIn,
391
- 10, // Allow up to 10 wei rounding error across all operations.
392
- "total paid in should equal reclaimed + remaining balance + fees"
393
- );
394
-
395
- // No value created from thin air: accounted total should never exceed what was paid in.
396
- assertLe(accountedFor, totalPaidIn + 10, "should not create value from nothing");
397
- }
398
-
399
- /// @notice Bonding curve consistency: cashing out a fraction should always return less than the proportional
400
- /// share when there is a nonzero cash out tax rate.
401
- function test_bondingCurve_subproportionalReclaim() public {
402
- address payer = makeAddr("bc_payer");
403
- vm.deal(payer, 100e18);
404
-
405
- // Pay 50 ETH.
406
- vm.prank(payer);
407
- uint256 totalTokens =
408
- jbMultiTerminal().pay{value: 50e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 50e18, payer, 0, "", "");
409
-
410
- // Cash out half the tokens.
411
- uint256 halfTokens = totalTokens / 2;
412
- vm.prank(payer);
413
- uint256 reclaimedHalf = jbMultiTerminal()
414
- .cashOutTokensOf({
415
- holder: payer,
416
- projectId: REVNET_ID,
417
- cashOutCount: halfTokens,
418
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
419
- minTokensReclaimed: 0,
420
- beneficiary: payable(payer),
421
- metadata: ""
422
- });
423
-
424
- // With a 50% tax rate and being the only holder, cashing out half the tokens
425
- // should return less than half the surplus (bonding curve subproportional behavior).
426
- // The terminal balance before cash out is 50 ETH (minus any reserved token splits).
427
- uint256 terminalBalanceBefore = 50e18; // Approximate -- the actual might differ slightly due to reserved
428
- // splits.
429
- assertLt(
430
- reclaimedHalf,
431
- terminalBalanceBefore / 2,
432
- "half-cash-out should return less than half the balance with nonzero tax"
433
- );
434
- assertGt(reclaimedHalf, 0, "should still reclaim something");
435
- }
436
-
437
- /// @notice After many operations (pay, cash out, pay again), the terminal balance should always be >= 0
438
- /// and the total supply should remain consistent.
439
- function test_extendedOperation_balanceAndSupplyConsistency() public {
440
- address[] memory users = _makeUsers(5);
441
-
442
- // Round 1: Everyone pays.
443
- for (uint256 i; i < 5; i++) {
444
- vm.prank(users[i]);
445
- jbMultiTerminal().pay{value: 5e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 5e18, users[i], 0, "", "");
446
- }
447
-
448
- // Round 2: Users 0-1 cash out half.
449
- for (uint256 i; i < 2; i++) {
450
- uint256 userBalance = jbTokens().totalBalanceOf(users[i], REVNET_ID);
451
- if (userBalance > 0) {
452
- vm.prank(users[i]);
453
- jbMultiTerminal()
454
- .cashOutTokensOf({
455
- holder: users[i],
456
- projectId: REVNET_ID,
457
- cashOutCount: userBalance / 2,
458
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
459
- minTokensReclaimed: 0,
460
- beneficiary: payable(users[i]),
461
- metadata: ""
462
- });
463
- }
464
- }
465
-
466
- // Warp to stage 1.
467
- vm.warp(block.timestamp + 365 days);
468
-
469
- // Round 3: More payments.
470
- for (uint256 i; i < 5; i++) {
471
- vm.prank(users[i]);
472
- jbMultiTerminal().pay{value: 3e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 3e18, users[i], 0, "", "");
473
- }
474
-
475
- // Round 4: User 3 cashes out everything.
476
- {
477
- uint256 user3Balance = jbTokens().totalBalanceOf(users[3], REVNET_ID);
478
- if (user3Balance > 0) {
479
- vm.prank(users[3]);
480
- jbMultiTerminal()
481
- .cashOutTokensOf({
482
- holder: users[3],
483
- projectId: REVNET_ID,
484
- cashOutCount: user3Balance,
485
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
486
- minTokensReclaimed: 0,
487
- beneficiary: payable(users[3]),
488
- metadata: ""
489
- });
490
- }
491
- }
492
-
493
- // Warp to stage 2.
494
- vm.warp(block.timestamp + 365 days);
495
-
496
- // Round 5: Final payments.
497
- for (uint256 i; i < 3; i++) {
498
- vm.prank(users[i]);
499
- jbMultiTerminal().pay{value: 1e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 1e18, users[i], 0, "", "");
500
- }
501
-
502
- // Final checks.
503
- uint256 terminalBalance =
504
- jbTerminalStore().balanceOf(address(jbMultiTerminal()), REVNET_ID, JBConstants.NATIVE_TOKEN);
505
- uint256 totalSupply = jbTokens().totalSupplyOf(REVNET_ID);
506
-
507
- assertGt(terminalBalance, 0, "terminal balance should be positive after all operations");
508
- assertGt(totalSupply, 0, "total supply should be positive after all operations");
509
- }
510
-
511
- /// @notice Issuance decay: within a single stage, payments further apart in time should yield fewer tokens.
512
- function test_issuanceDecay_withinStage() public {
513
- address user = makeAddr("decay_user");
514
- vm.deal(user, 1000e18);
515
-
516
- // Pay 1 ETH now.
517
- vm.prank(user);
518
- uint256 tokensBefore =
519
- jbMultiTerminal().pay{value: 1e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 1e18, user, 0, "", "");
520
-
521
- // Warp 180 days (2 cut periods of 90 days each).
522
- vm.warp(block.timestamp + 180 days);
523
-
524
- // Pay 1 ETH again.
525
- address user2 = makeAddr("decay_user2");
526
- vm.deal(user2, 100e18);
527
- vm.prank(user2);
528
- uint256 tokensAfter =
529
- jbMultiTerminal().pay{value: 1e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 1e18, user2, 0, "", "");
530
-
531
- // Stage 0 has 50% cut per 90-day cycle. After 2 cycles, issuance should be ~25% of original.
532
- assertGt(tokensBefore, tokensAfter, "earlier payment should receive more tokens due to issuance decay");
533
- }
534
-
535
- /// @notice Late entrants cannot extract more value than they put in, even with many prior participants.
536
- function test_noValueExtraction_byLateEntrant() public {
537
- address[] memory earlyUsers = _makeUsers(5);
538
-
539
- // Early users pay 10 ETH each.
540
- for (uint256 i; i < 5; i++) {
541
- vm.prank(earlyUsers[i]);
542
- jbMultiTerminal().pay{value: 10e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 10e18, earlyUsers[i], 0, "", "");
543
- }
544
-
545
- // Warp to stage 1 to change the cash out tax rate.
546
- vm.warp(block.timestamp + 365 days);
547
-
548
- // Late entrant pays 10 ETH.
549
- address lateUser = makeAddr("late_user");
550
- vm.deal(lateUser, 100e18);
551
- vm.prank(lateUser);
552
- uint256 lateTokens =
553
- jbMultiTerminal().pay{value: 10e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 10e18, lateUser, 0, "", "");
554
-
555
- // Late entrant immediately tries to cash out everything.
556
- vm.prank(lateUser);
557
- uint256 reclaimed = jbMultiTerminal()
558
- .cashOutTokensOf({
559
- holder: lateUser,
560
- projectId: REVNET_ID,
561
- cashOutCount: lateTokens,
562
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
563
- minTokensReclaimed: 0,
564
- beneficiary: payable(lateUser),
565
- metadata: ""
566
- });
567
-
568
- // The late entrant should not extract more than they put in.
569
- assertLe(reclaimed, 10e18, "late entrant should not extract more than they paid");
570
- }
571
-
572
- /// @notice After a full lifecycle through all 3 stages with many operations, verify the terminal balance
573
- /// is always non-negative and equals the actual ETH held by the terminal contract.
574
- function test_terminalBalanceMatchesActualEth() public {
575
- address[] memory users = _makeUsers(3);
576
-
577
- // Stage 0.
578
- for (uint256 i; i < 20; i++) {
579
- address user = users[i % 3];
580
- vm.prank(user);
581
- jbMultiTerminal().pay{value: 1e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 1e18, user, 0, "", "");
582
- vm.warp(block.timestamp + 10 days);
583
- }
584
-
585
- // Warp to stage 1.
586
- vm.warp(block.timestamp + 200 days);
587
-
588
- // Stage 1: pay and cash out.
589
- for (uint256 i; i < 10; i++) {
590
- address user = users[i % 3];
591
- vm.prank(user);
592
- jbMultiTerminal().pay{value: 2e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 2e18, user, 0, "", "");
593
- }
594
-
595
- // Cash out some.
596
- {
597
- uint256 balance0 = jbTokens().totalBalanceOf(users[0], REVNET_ID);
598
- if (balance0 > 0) {
599
- vm.prank(users[0]);
600
- jbMultiTerminal()
601
- .cashOutTokensOf({
602
- holder: users[0],
603
- projectId: REVNET_ID,
604
- cashOutCount: balance0 / 3,
605
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
606
- minTokensReclaimed: 0,
607
- beneficiary: payable(users[0]),
608
- metadata: ""
609
- });
610
- }
611
- }
612
-
613
- // Warp to stage 2.
614
- vm.warp(block.timestamp + 2 * 365 days);
615
-
616
- // Stage 2: minimal payments.
617
- for (uint256 i; i < 5; i++) {
618
- address user = users[i % 3];
619
- vm.prank(user);
620
- jbMultiTerminal().pay{value: 0.5e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 0.5e18, user, 0, "", "");
621
- }
622
-
623
- // Verify the recorded balance matches the terminal's actual ETH holdings.
624
- // The terminal holds ETH for ALL projects, so we check that the recorded balance
625
- // for our revnet does not exceed the terminal's total ETH.
626
- uint256 recordedBalance =
627
- jbTerminalStore().balanceOf(address(jbMultiTerminal()), REVNET_ID, JBConstants.NATIVE_TOKEN);
628
- uint256 terminalEth = address(jbMultiTerminal()).balance;
629
-
630
- assertGt(recordedBalance, 0, "recorded balance should be positive");
631
- assertLe(recordedBalance, terminalEth, "recorded balance should not exceed terminal's actual ETH");
632
- }
633
-
634
- /// @notice Monotonically increasing fee project balance: every cash out with nonzero tax should increase
635
- /// the fee project's balance.
636
- function test_feeProjectBalance_monotonicallyIncreases() public {
637
- address user = makeAddr("fee_check_user");
638
- vm.deal(user, 1000e18);
639
-
640
- // Pay 100 ETH.
641
- vm.prank(user);
642
- uint256 tokens =
643
- jbMultiTerminal().pay{value: 100e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 100e18, user, 0, "", "");
644
-
645
- uint256 feeBalanceBefore =
646
- jbTerminalStore().balanceOf(address(jbMultiTerminal()), FEE_PROJECT_ID, JBConstants.NATIVE_TOKEN);
647
-
648
- // Cash out portions in 5 rounds.
649
- uint256 portion = tokens / 6;
650
- for (uint256 i; i < 5; i++) {
651
- uint256 feeBalanceBeforeRound =
652
- jbTerminalStore().balanceOf(address(jbMultiTerminal()), FEE_PROJECT_ID, JBConstants.NATIVE_TOKEN);
653
-
654
- vm.prank(user);
655
- jbMultiTerminal()
656
- .cashOutTokensOf({
657
- holder: user,
658
- projectId: REVNET_ID,
659
- cashOutCount: portion,
660
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
661
- minTokensReclaimed: 0,
662
- beneficiary: payable(user),
663
- metadata: ""
664
- });
665
-
666
- uint256 feeBalanceAfterRound =
667
- jbTerminalStore().balanceOf(address(jbMultiTerminal()), FEE_PROJECT_ID, JBConstants.NATIVE_TOKEN);
668
-
669
- // Fee balance should increase (or at least not decrease) after each cash out.
670
- assertGe(feeBalanceAfterRound, feeBalanceBeforeRound, "fee project balance should monotonically increase");
671
- }
672
-
673
- uint256 feeBalanceAfter =
674
- jbTerminalStore().balanceOf(address(jbMultiTerminal()), FEE_PROJECT_ID, JBConstants.NATIVE_TOKEN);
675
- assertGt(feeBalanceAfter, feeBalanceBefore, "fee project should have earned fees from cash outs");
676
- }
677
- }