@rev-net/core-v6 0.0.14 → 0.0.15

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.
@@ -0,0 +1,148 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
3
+
4
+ // forge-lint: disable-next-line(unaliased-plain-import)
5
+ import "./ForkTestBase.sol";
6
+ import {REVEmpty721Config} from "../helpers/REVEmpty721Config.sol";
7
+
8
+ /// @notice Fork tests for revnet auto-issuance (per-stage premint) mechanics.
9
+ ///
10
+ /// Verifies that `autoIssueFor()` mints tokens to the beneficiary at the correct time,
11
+ /// bypasses the reserved percent, and properly prevents double claims and early claims.
12
+ ///
13
+ /// Run with: FOUNDRY_PROFILE=fork forge test --match-contract TestAutoIssuanceFork -vvv
14
+ contract TestAutoIssuanceFork is ForkTestBase {
15
+ // forge-lint: disable-next-line(mixed-case-variable)
16
+ address AUTO_BENEFICIARY = makeAddr("autoBeneficiary");
17
+ uint104 constant AUTO_ISSUE_COUNT = 500e18; // 500 tokens
18
+
19
+ /// @notice Deploy a revnet with auto-issuance configured for the first stage.
20
+ /// @param splitPercent The reserved percent (splitPercent) for the stage.
21
+ /// @param startsInFuture If true, the stage starts 1 day in the future; otherwise starts now.
22
+ /// @return revnetId The deployed revnet's project ID.
23
+ /// @return stageId The stage ID (ruleset ID) for the auto-issuance.
24
+ function _deployRevnetWithAutoIssuance(
25
+ uint16 splitPercent,
26
+ bool startsInFuture
27
+ )
28
+ internal
29
+ returns (uint256 revnetId, uint256 stageId)
30
+ {
31
+ JBAccountingContext[] memory acc = new JBAccountingContext[](1);
32
+ acc[0] = JBAccountingContext({
33
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
34
+ });
35
+
36
+ JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
37
+ tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
38
+
39
+ // Configure auto-issuance for this chain.
40
+ REVAutoIssuance[] memory autoIssuances = new REVAutoIssuance[](1);
41
+ autoIssuances[0] =
42
+ REVAutoIssuance({chainId: uint32(block.chainid), count: AUTO_ISSUE_COUNT, beneficiary: AUTO_BENEFICIARY});
43
+
44
+ REVStageConfig[] memory stages = new REVStageConfig[](1);
45
+ JBSplit[] memory splits = new JBSplit[](1);
46
+ splits[0].beneficiary = payable(multisig());
47
+ splits[0].percent = 10_000;
48
+
49
+ uint48 startTime = startsInFuture ? uint48(block.timestamp + 1 days) : uint48(block.timestamp);
50
+
51
+ stages[0] = REVStageConfig({
52
+ startsAtOrAfter: uint40(startTime),
53
+ autoIssuances: autoIssuances,
54
+ splitPercent: splitPercent,
55
+ splits: splits,
56
+ initialIssuance: INITIAL_ISSUANCE,
57
+ issuanceCutFrequency: 0,
58
+ issuanceCutPercent: 0,
59
+ cashOutTaxRate: 5000,
60
+ extraMetadata: 0
61
+ });
62
+
63
+ REVConfig memory cfg = REVConfig({
64
+ description: REVDescription("AutoIssue Test", "AUTO", "ipfs://auto", "AUTO_SALT"),
65
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
66
+ splitOperator: multisig(),
67
+ stageConfigurations: stages
68
+ });
69
+
70
+ REVSuckerDeploymentConfig memory sdc = REVSuckerDeploymentConfig({
71
+ deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("AUTO_TEST"))
72
+ });
73
+
74
+ // The stageId is block.timestamp + 0 (first stage index is 0).
75
+ stageId = block.timestamp;
76
+
77
+ (revnetId,) = REV_DEPLOYER.deployFor({
78
+ revnetId: 0,
79
+ configuration: cfg,
80
+ terminalConfigurations: tc,
81
+ suckerDeploymentConfiguration: sdc,
82
+ tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
83
+ allowedPosts: REVEmpty721Config.emptyAllowedPosts()
84
+ });
85
+ }
86
+
87
+ function setUp() public override {
88
+ super.setUp();
89
+ _deployFeeProject(5000);
90
+ }
91
+
92
+ /// @notice Calling autoIssueFor before the stage starts should revert with REVDeployer_StageNotStarted.
93
+ function testFork_AutoIssueBeforeStageReverts() public {
94
+ (uint256 revnetId, uint256 stageId) = _deployRevnetWithAutoIssuance({splitPercent: 0, startsInFuture: true});
95
+
96
+ // Stage starts in the future, so autoIssueFor should revert.
97
+ vm.expectRevert(abi.encodeWithSelector(REVDeployer.REVDeployer_StageNotStarted.selector, stageId));
98
+ REV_DEPLOYER.autoIssueFor(revnetId, stageId, AUTO_BENEFICIARY);
99
+ }
100
+
101
+ /// @notice After the stage starts, autoIssueFor mints the exact configured count to the beneficiary.
102
+ function testFork_AutoIssueAfterStageStart() public {
103
+ (uint256 revnetId, uint256 stageId) = _deployRevnetWithAutoIssuance({splitPercent: 0, startsInFuture: false});
104
+
105
+ // Verify beneficiary has no tokens initially.
106
+ uint256 balanceBefore = jbTokens().totalBalanceOf(AUTO_BENEFICIARY, revnetId);
107
+ assertEq(balanceBefore, 0, "beneficiary should have no tokens before auto-issue");
108
+
109
+ // Auto-issue tokens.
110
+ REV_DEPLOYER.autoIssueFor(revnetId, stageId, AUTO_BENEFICIARY);
111
+
112
+ // Verify beneficiary received exactly AUTO_ISSUE_COUNT tokens.
113
+ uint256 balanceAfter = jbTokens().totalBalanceOf(AUTO_BENEFICIARY, revnetId);
114
+ assertEq(balanceAfter, AUTO_ISSUE_COUNT, "beneficiary should receive exactly the configured token count");
115
+ }
116
+
117
+ /// @notice A second call to autoIssueFor for the same beneficiary/stage reverts with NothingToAutoIssue.
118
+ function testFork_AutoIssueDoubleClaimReverts() public {
119
+ (uint256 revnetId, uint256 stageId) = _deployRevnetWithAutoIssuance({splitPercent: 0, startsInFuture: false});
120
+
121
+ // First claim succeeds.
122
+ REV_DEPLOYER.autoIssueFor(revnetId, stageId, AUTO_BENEFICIARY);
123
+
124
+ // Second claim should revert — amount was reset to 0.
125
+ vm.expectRevert(REVDeployer.REVDeployer_NothingToAutoIssue.selector);
126
+ REV_DEPLOYER.autoIssueFor(revnetId, stageId, AUTO_BENEFICIARY);
127
+ }
128
+
129
+ /// @notice Auto-issued tokens bypass the reserved percent — 100% goes to the beneficiary.
130
+ function testFork_AutoIssueBypassesReservedPercent() public {
131
+ // Deploy with 50% splitPercent (reserved percent).
132
+ (uint256 revnetId, uint256 stageId) = _deployRevnetWithAutoIssuance({splitPercent: 5000, startsInFuture: false});
133
+
134
+ // Auto-issue tokens.
135
+ REV_DEPLOYER.autoIssueFor(revnetId, stageId, AUTO_BENEFICIARY);
136
+
137
+ // The beneficiary should receive the FULL count, not reduced by reserved percent.
138
+ uint256 balance = jbTokens().totalBalanceOf(AUTO_BENEFICIARY, revnetId);
139
+ assertEq(
140
+ balance, AUTO_ISSUE_COUNT, "auto-issued tokens should bypass reserved percent - full amount to beneficiary"
141
+ );
142
+
143
+ // Verify no pending reserved tokens were accumulated from the auto-issue.
144
+ // The mintTokensOf call uses useReservedPercent: false, so pendingReservedTokenBalanceOf should be 0.
145
+ uint256 pending = jbController().pendingReservedTokenBalanceOf(revnetId);
146
+ assertEq(pending, 0, "no reserved tokens should be pending from auto-issue");
147
+ }
148
+ }
@@ -40,9 +40,10 @@ contract TestCashOutFork is ForkTestBase {
40
40
  uint256 feeTerminalBefore = _terminalBalance(FEE_PROJECT_ID, JBConstants.NATIVE_TOKEN);
41
41
  uint256 totalSupplyBefore = jbTokens().totalSupplyOf(revnetId);
42
42
 
43
- // Cash out.
43
+ // Cash out. When the buyback hook finds a better swap route, it sets cashOutTaxRate = MAX
44
+ // so reclaimedAmount (direct reclaim) is 0 — beneficiary gets ETH via hook swap instead.
44
45
  vm.prank(PAYER);
45
- uint256 reclaimedAmount = jbMultiTerminal()
46
+ jbMultiTerminal()
46
47
  .cashOutTokensOf({
47
48
  holder: PAYER,
48
49
  projectId: revnetId,
@@ -53,25 +54,18 @@ contract TestCashOutFork is ForkTestBase {
53
54
  metadata: ""
54
55
  });
55
56
 
56
- // Payer received ETH.
57
- assertGt(PAYER.balance, payerEthBefore, "payer should receive ETH");
58
- assertEq(PAYER.balance - payerEthBefore, reclaimedAmount, "reclaimed amount should match balance change");
57
+ // Payer received ETH (via buyback hook swap).
58
+ uint256 ethReceived = PAYER.balance - payerEthBefore;
59
+ assertGt(ethReceived, 0, "payer should receive ETH");
59
60
 
60
- // Fee project terminal balance increased (2.5% fee on the cashout portion processed by the hook).
61
+ // Fee project terminal balance increased (2.5% fee on the cashout portion processed by REVDeployer hook).
61
62
  uint256 feeTerminalAfter = _terminalBalance(FEE_PROJECT_ID, JBConstants.NATIVE_TOKEN);
62
63
  assertGt(feeTerminalAfter, feeTerminalBefore, "fee project should receive fee");
63
64
 
64
- // Token supply decreased.
65
+ // When the sell-side buyback route is used, the hook mints tokens to sell into the pool,
66
+ // so net token supply stays the same (burn + re-mint). Verify supply didn't increase.
65
67
  uint256 totalSupplyAfter = jbTokens().totalSupplyOf(revnetId);
66
- assertEq(totalSupplyAfter, totalSupplyBefore - cashOutCount, "total supply should decrease by cashOutCount");
67
-
68
- // Reclaim is less than pro-rata share due to 50% tax rate.
69
- uint256 surplus = _terminalBalance(revnetId, JBConstants.NATIVE_TOKEN) + reclaimedAmount
70
- + (feeTerminalAfter - feeTerminalBefore);
71
- // Pro-rata share = surplus * cashOutCount / totalSupply
72
- // With 50% tax, reclaim should be roughly 75% of pro-rata (from bonding curve formula).
73
- uint256 proRataShare = (surplus * cashOutCount) / totalSupplyBefore;
74
- assertLt(reclaimedAmount, proRataShare, "reclaim should be less than pro-rata due to tax");
68
+ assertLe(totalSupplyAfter, totalSupplyBefore, "total supply should not increase");
75
69
  }
76
70
 
77
71
  /// @notice High tax rate (90%) produces small reclaim relative to pro-rata.
@@ -181,9 +175,13 @@ contract TestCashOutFork is ForkTestBase {
181
175
  assertGt(terminalBalance, 0, "terminal should have balance");
182
176
 
183
177
  // Cash out should succeed using actual terminal balance as surplus.
178
+ // When the buyback hook finds a better swap route it sets cashOutTaxRate = MAX, so the terminal's
179
+ // direct reclaimAmount is 0 — the beneficiary receives ETH via the hook's swap instead.
184
180
  if (payerTokens > 0) {
181
+ uint256 payerEthBefore = PAYER.balance;
182
+
185
183
  vm.prank(PAYER);
186
- uint256 reclaimedAmount = jbMultiTerminal()
184
+ jbMultiTerminal()
187
185
  .cashOutTokensOf({
188
186
  holder: PAYER,
189
187
  projectId: splitRevnetId,
@@ -194,7 +192,7 @@ contract TestCashOutFork is ForkTestBase {
194
192
  metadata: ""
195
193
  });
196
194
 
197
- assertGt(reclaimedAmount, 0, "should reclaim some ETH after tier split payment");
195
+ assertGt(PAYER.balance, payerEthBefore, "payer should receive ETH after tier split cashout");
198
196
  }
199
197
  }
200
198
 
@@ -234,9 +232,12 @@ contract TestCashOutFork is ForkTestBase {
234
232
  // Warp past delay.
235
233
  vm.warp(block.timestamp + REV_DEPLOYER.CASH_OUT_DELAY() + 1);
236
234
 
237
- // Now it should succeed.
235
+ // Now it should succeed. When the buyback hook routes via swap, reclaimAmount is 0 but
236
+ // the beneficiary receives ETH through the hook.
237
+ uint256 payerEthBefore = PAYER.balance;
238
+
238
239
  vm.prank(PAYER);
239
- uint256 reclaimedAmount = jbMultiTerminal()
240
+ jbMultiTerminal()
240
241
  .cashOutTokensOf({
241
242
  holder: PAYER,
242
243
  projectId: delayRevnet,
@@ -247,6 +248,6 @@ contract TestCashOutFork is ForkTestBase {
247
248
  metadata: ""
248
249
  });
249
250
 
250
- assertGt(reclaimedAmount, 0, "should succeed after delay expires");
251
+ assertGt(PAYER.balance, payerEthBefore, "should succeed after delay expires");
251
252
  }
252
253
  }
@@ -0,0 +1,158 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
3
+
4
+ // forge-lint: disable-next-line(unaliased-plain-import)
5
+ import "./ForkTestBase.sol";
6
+ import {REVEmpty721Config} from "../helpers/REVEmpty721Config.sol";
7
+
8
+ /// @notice Fork tests for revnet issuance decay (weight cut) mechanics.
9
+ ///
10
+ /// Verifies that `issuanceCutFrequency` maps to `JBRulesetConfig.duration` and
11
+ /// `issuanceCutPercent` maps to `weightCutPercent`, producing geometric token decay.
12
+ ///
13
+ /// Run with: FOUNDRY_PROFILE=fork forge test --match-contract TestIssuanceDecayFork -vvv
14
+ contract TestIssuanceDecayFork is ForkTestBase {
15
+ /// @notice Deploy a revnet with custom issuance cut parameters.
16
+ /// @param issuanceCutFrequency The duration in seconds between decay steps (maps to ruleset duration).
17
+ /// @param issuanceCutPercent The percentage to cut issuance each cycle (out of 1_000_000_000).
18
+ /// @param cashOutTaxRate The cash out tax rate.
19
+ /// @return revnetId The deployed revnet's project ID.
20
+ function _deployRevnetWithDecay(
21
+ uint32 issuanceCutFrequency,
22
+ uint32 issuanceCutPercent,
23
+ uint16 cashOutTaxRate
24
+ )
25
+ internal
26
+ returns (uint256 revnetId)
27
+ {
28
+ JBAccountingContext[] memory acc = new JBAccountingContext[](1);
29
+ acc[0] = JBAccountingContext({
30
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
31
+ });
32
+
33
+ JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
34
+ tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
35
+
36
+ REVStageConfig[] memory stages = new REVStageConfig[](1);
37
+ JBSplit[] memory splits = new JBSplit[](1);
38
+ splits[0].beneficiary = payable(multisig());
39
+ splits[0].percent = 10_000;
40
+ stages[0] = REVStageConfig({
41
+ startsAtOrAfter: uint40(block.timestamp),
42
+ autoIssuances: new REVAutoIssuance[](0),
43
+ splitPercent: 0,
44
+ splits: splits,
45
+ initialIssuance: INITIAL_ISSUANCE,
46
+ issuanceCutFrequency: issuanceCutFrequency,
47
+ issuanceCutPercent: issuanceCutPercent,
48
+ cashOutTaxRate: cashOutTaxRate,
49
+ extraMetadata: 0
50
+ });
51
+
52
+ REVConfig memory cfg = REVConfig({
53
+ description: REVDescription("Decay Test", "DECAY", "ipfs://decay", "DECAY_SALT"),
54
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
55
+ splitOperator: multisig(),
56
+ stageConfigurations: stages
57
+ });
58
+
59
+ REVSuckerDeploymentConfig memory sdc = REVSuckerDeploymentConfig({
60
+ deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("DECAY_TEST"))
61
+ });
62
+
63
+ (revnetId,) = REV_DEPLOYER.deployFor({
64
+ revnetId: 0,
65
+ configuration: cfg,
66
+ terminalConfigurations: tc,
67
+ suckerDeploymentConfiguration: sdc,
68
+ tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
69
+ allowedPosts: REVEmpty721Config.emptyAllowedPosts()
70
+ });
71
+ }
72
+
73
+ function setUp() public override {
74
+ super.setUp();
75
+ _deployFeeProject(5000);
76
+ }
77
+
78
+ /// @notice After one cycle with 10% issuance cut, paying 1 ETH yields ~90% of the day-0 tokens.
79
+ function testFork_IssuanceDecaysSingleCycle() public {
80
+ // 10% cut per cycle, 1-day cycles.
81
+ uint256 revnetId = _deployRevnetWithDecay({
82
+ issuanceCutFrequency: 86_400, // 1 day
83
+ issuanceCutPercent: 100_000_000, // 10% of 1e9
84
+ cashOutTaxRate: 5000
85
+ });
86
+
87
+ // Set up pool so buyback hook doesn't interfere (mint path wins at 1:1).
88
+ _setupPool(revnetId, 10_000 ether);
89
+
90
+ // Day 0: pay 1 ETH.
91
+ uint256 t0 = _payRevnet(revnetId, PAYER, 1 ether);
92
+ assertGt(t0, 0, "day 0 tokens should be > 0");
93
+
94
+ // Warp 1 day (one full cycle).
95
+ vm.warp(block.timestamp + 86_400);
96
+
97
+ // Day 1: pay 1 ETH again.
98
+ address payer2 = makeAddr("payer2");
99
+ vm.deal(payer2, 10 ether);
100
+ uint256 t1 = _payRevnet(revnetId, payer2, 1 ether);
101
+
102
+ // T1 should be approximately T0 * 0.9 (within 1% tolerance for rounding).
103
+ uint256 expected = (t0 * 9) / 10;
104
+ uint256 tolerance = expected / 100; // 1%
105
+ assertGt(t1, expected - tolerance, "T1 should be >= T0*0.9 - 1%");
106
+ assertLt(t1, expected + tolerance, "T1 should be <= T0*0.9 + 1%");
107
+ }
108
+
109
+ /// @notice After two cycles with 10% issuance cut, paying 1 ETH yields ~81% of the day-0 tokens.
110
+ function testFork_IssuanceDecaysMultipleCycles() public {
111
+ // 10% cut per cycle, 1-day cycles.
112
+ uint256 revnetId = _deployRevnetWithDecay({
113
+ issuanceCutFrequency: 86_400, issuanceCutPercent: 100_000_000, cashOutTaxRate: 5000
114
+ });
115
+
116
+ _setupPool(revnetId, 10_000 ether);
117
+
118
+ // Day 0: pay 1 ETH to establish baseline.
119
+ uint256 t0 = _payRevnet(revnetId, PAYER, 1 ether);
120
+
121
+ // Warp 2 days (two full cycles).
122
+ vm.warp(block.timestamp + 86_400 * 2);
123
+
124
+ // Day 2: pay 1 ETH.
125
+ address payer2 = makeAddr("payer2");
126
+ vm.deal(payer2, 10 ether);
127
+ uint256 t2 = _payRevnet(revnetId, payer2, 1 ether);
128
+
129
+ // T2 should be approximately T0 * 0.9 * 0.9 = T0 * 0.81 (within 1% tolerance).
130
+ uint256 expected = (t0 * 81) / 100;
131
+ uint256 tolerance = expected / 100; // 1%
132
+ assertGt(t2, expected - tolerance, "T2 should be >= T0*0.81 - 1%");
133
+ assertLt(t2, expected + tolerance, "T2 should be <= T0*0.81 + 1%");
134
+ }
135
+
136
+ /// @notice With 0% issuance cut, weight never changes and T0 == T1.
137
+ function testFork_IssuanceDecayZeroPercent() public {
138
+ // 0% cut per cycle, 1-day cycles.
139
+ uint256 revnetId =
140
+ _deployRevnetWithDecay({issuanceCutFrequency: 86_400, issuanceCutPercent: 0, cashOutTaxRate: 5000});
141
+
142
+ _setupPool(revnetId, 10_000 ether);
143
+
144
+ // Day 0: pay 1 ETH.
145
+ uint256 t0 = _payRevnet(revnetId, PAYER, 1 ether);
146
+
147
+ // Warp 1 day.
148
+ vm.warp(block.timestamp + 86_400);
149
+
150
+ // Day 1: pay 1 ETH.
151
+ address payer2 = makeAddr("payer2");
152
+ vm.deal(payer2, 10 ether);
153
+ uint256 t1 = _payRevnet(revnetId, payer2, 1 ether);
154
+
155
+ // With 0% cut, tokens should be identical.
156
+ assertEq(t1, t0, "with 0% cut, tokens should be the same across cycles");
157
+ }
158
+ }