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