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