@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.
- package/CHANGELOG.md +2 -2
- package/README.md +6 -7
- package/foundry.toml +1 -1
- package/package.json +23 -16
- package/references/operations.md +1 -1
- package/references/runtime.md +1 -1
- package/script/Deploy.s.sol +12 -9
- package/src/REVDeployer.sol +60 -65
- package/src/REVHiddenTokens.sol +2 -2
- package/src/REVLoans.sol +134 -90
- package/src/REVOwner.sol +124 -17
- package/src/interfaces/IREVDeployer.sol +2 -1
- package/src/interfaces/IREVHiddenTokens.sol +4 -1
- package/src/interfaces/IREVOwner.sol +5 -0
- package/ADMINISTRATION.md +0 -73
- package/ARCHITECTURE.md +0 -116
- package/AUDIT_INSTRUCTIONS.md +0 -90
- package/RISKS.md +0 -97
- package/SKILLS.md +0 -46
- package/STYLE_GUIDE.md +0 -610
- package/USER_JOURNEYS.md +0 -195
- package/foundry.lock +0 -11
- package/slither-ci.config.json +0 -10
- package/sphinx.lock +0 -507
- package/test/REV.integrations.t.sol +0 -573
- package/test/REVAutoIssuanceFuzz.t.sol +0 -328
- package/test/REVDeployerRegressions.t.sol +0 -396
- package/test/REVInvincibility.t.sol +0 -1371
- package/test/REVInvincibilityHandler.sol +0 -387
- package/test/REVLifecycle.t.sol +0 -420
- package/test/REVLoans.invariants.t.sol +0 -724
- package/test/REVLoansAttacks.t.sol +0 -816
- package/test/REVLoansFeeRecovery.t.sol +0 -783
- package/test/REVLoansFindings.t.sol +0 -711
- package/test/REVLoansRegressions.t.sol +0 -364
- package/test/REVLoansSourceFeeRecovery.t.sol +0 -517
- package/test/REVLoansSourced.t.sol +0 -1839
- package/test/REVLoansUnSourced.t.sol +0 -409
- package/test/TestAuditFixVerification.t.sol +0 -675
- package/test/TestBurnHeldTokens.t.sol +0 -394
- package/test/TestCEIPattern.t.sol +0 -508
- package/test/TestCashOutCallerValidation.t.sol +0 -452
- package/test/TestConversionDocumentation.t.sol +0 -368
- package/test/TestCrossCurrencyReclaim.t.sol +0 -610
- package/test/TestCrossSourceReallocation.t.sol +0 -361
- package/test/TestERC2771MetaTx.t.sol +0 -585
- package/test/TestEmptyBuybackSpecs.t.sol +0 -300
- package/test/TestFlashLoanSurplus.t.sol +0 -365
- package/test/TestHiddenTokens.t.sol +0 -474
- package/test/TestHookArrayOOB.t.sol +0 -278
- package/test/TestLiquidationBehavior.t.sol +0 -398
- package/test/TestLoanSourceRotation.t.sol +0 -553
- package/test/TestLoansCashOutDelay.t.sol +0 -493
- package/test/TestLongTailEconomics.t.sol +0 -677
- package/test/TestLowFindings.t.sol +0 -677
- package/test/TestMixedFixes.t.sol +0 -593
- package/test/TestPermit2Signatures.t.sol +0 -683
- package/test/TestReallocationSandwich.t.sol +0 -412
- package/test/TestRevnetRegressions.t.sol +0 -350
- package/test/TestSplitWeightAdjustment.t.sol +0 -527
- package/test/TestSplitWeightE2E.t.sol +0 -605
- package/test/TestSplitWeightFork.t.sol +0 -855
- package/test/TestStageTransitionBorrowable.t.sol +0 -301
- package/test/TestSwapTerminalPermission.t.sol +0 -262
- package/test/TestTerminalEncodingInHash.t.sol +0 -326
- package/test/TestUint112Overflow.t.sol +0 -311
- package/test/TestZeroAmountLoanGuard.t.sol +0 -378
- package/test/TestZeroRepayment.t.sol +0 -354
- package/test/audit/CodexCrossChainBuybackRouteMismatch.t.sol +0 -184
- package/test/audit/CodexPhantomSurplusTerminal.t.sol +0 -367
- package/test/audit/CodexREVOwnerRemoteSurplusCurrencyMismatch.t.sol +0 -142
- package/test/audit/LoanIdOverflowGuard.t.sol +0 -523
- package/test/audit/NemesisOperatorDelegation.t.sol +0 -356
- package/test/audit/SupportsInterfaceTest.t.sol +0 -51
- package/test/audit/TestFeeAllowanceLeak.t.sol +0 -197
- package/test/audit/TestLoansAndDeployerFixes.t.sol +0 -576
- package/test/fork/ForkTestBase.sol +0 -727
- package/test/fork/TestAutoIssuanceFork.t.sol +0 -148
- package/test/fork/TestCashOutFork.t.sol +0 -253
- package/test/fork/TestIssuanceDecayFork.t.sol +0 -158
- package/test/fork/TestLoanBorrowFork.t.sol +0 -163
- package/test/fork/TestLoanCrossRulesetFork.t.sol +0 -308
- package/test/fork/TestLoanERC20Fork.t.sol +0 -465
- package/test/fork/TestLoanLiquidationFork.t.sol +0 -135
- package/test/fork/TestLoanReallocateFork.t.sol +0 -113
- package/test/fork/TestLoanRepayFork.t.sol +0 -188
- package/test/fork/TestLoanTransferFork.t.sol +0 -143
- package/test/fork/TestPermit2PaymentFork.t.sol +0 -300
- package/test/fork/TestSplitWeightFork.t.sol +0 -189
- package/test/helpers/MaliciousContracts.sol +0 -247
- package/test/helpers/REVEmpty721Config.sol +0 -45
- package/test/mock/MockBuybackCashOutRecorder.sol +0 -84
- package/test/mock/MockBuybackDataHook.sol +0 -112
- package/test/mock/MockBuybackDataHookMintPath.sol +0 -68
- package/test/mock/MockSuckerRegistry.sol +0 -17
- package/test/regression/TestBurnPermissionRequired.t.sol +0 -294
- package/test/regression/TestCashOutBuybackFeeLeak.t.sol +0 -232
- package/test/regression/TestCrossRevnetLiquidation.t.sol +0 -255
- package/test/regression/TestCumulativeLoanCounter.t.sol +0 -361
- package/test/regression/TestLiquidateGapHandling.t.sol +0 -394
- package/test/regression/TestZeroPriceFeed.t.sol +0 -422
|
@@ -1,148 +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 "./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
|
-
}
|
|
@@ -1,253 +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 "./ForkTestBase.sol";
|
|
6
|
-
import {REVEmpty721Config} from "../helpers/REVEmpty721Config.sol";
|
|
7
|
-
|
|
8
|
-
/// @notice Fork tests for revnet cash-out scenarios with real Uniswap V4 buyback hook.
|
|
9
|
-
///
|
|
10
|
-
/// Covers: fee deduction, high tax rate, sucker exemption, surplus after tier splits, and delay enforcement.
|
|
11
|
-
///
|
|
12
|
-
/// Run with: FOUNDRY_PROFILE=fork forge test --match-contract TestCashOutFork -vvv
|
|
13
|
-
contract TestCashOutFork is ForkTestBase {
|
|
14
|
-
uint256 revnetId;
|
|
15
|
-
|
|
16
|
-
function setUp() public override {
|
|
17
|
-
super.setUp();
|
|
18
|
-
|
|
19
|
-
// Deploy fee project + revnet with 50% cashOutTaxRate.
|
|
20
|
-
_deployFeeProject(5000);
|
|
21
|
-
revnetId = _deployRevnet(5000);
|
|
22
|
-
|
|
23
|
-
// Set up pool at 1:1 (mint path wins).
|
|
24
|
-
_setupPool(revnetId, 10_000 ether);
|
|
25
|
-
|
|
26
|
-
// Pay 10 ETH to create surplus and tokens.
|
|
27
|
-
_payRevnet(revnetId, PAYER, 10 ether);
|
|
28
|
-
|
|
29
|
-
// Warp past the 30-day cash-out delay.
|
|
30
|
-
vm.warp(block.timestamp + REV_DEPLOYER.CASH_OUT_DELAY() + 1);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/// @notice Cash out tokens and verify fee deduction, token burn, and bonding curve reclaim.
|
|
34
|
-
function test_fork_cashOut_normalWithFee() public {
|
|
35
|
-
uint256 payerTokens = jbTokens().totalBalanceOf(PAYER, revnetId);
|
|
36
|
-
uint256 cashOutCount = payerTokens / 2; // Cash out half.
|
|
37
|
-
|
|
38
|
-
// Record state before.
|
|
39
|
-
uint256 payerEthBefore = PAYER.balance;
|
|
40
|
-
uint256 feeTerminalBefore = _terminalBalance(FEE_PROJECT_ID, JBConstants.NATIVE_TOKEN);
|
|
41
|
-
uint256 totalSupplyBefore = jbTokens().totalSupplyOf(revnetId);
|
|
42
|
-
|
|
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.
|
|
45
|
-
vm.prank(PAYER);
|
|
46
|
-
jbMultiTerminal()
|
|
47
|
-
.cashOutTokensOf({
|
|
48
|
-
holder: PAYER,
|
|
49
|
-
projectId: revnetId,
|
|
50
|
-
cashOutCount: cashOutCount,
|
|
51
|
-
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
52
|
-
minTokensReclaimed: 0,
|
|
53
|
-
beneficiary: payable(PAYER),
|
|
54
|
-
metadata: ""
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
// Payer received ETH (via buyback hook swap).
|
|
58
|
-
uint256 ethReceived = PAYER.balance - payerEthBefore;
|
|
59
|
-
assertGt(ethReceived, 0, "payer should receive ETH");
|
|
60
|
-
|
|
61
|
-
// Fee project terminal balance increased (2.5% fee on the cashout portion processed by REVDeployer hook).
|
|
62
|
-
uint256 feeTerminalAfter = _terminalBalance(FEE_PROJECT_ID, JBConstants.NATIVE_TOKEN);
|
|
63
|
-
assertGt(feeTerminalAfter, feeTerminalBefore, "fee project should receive fee");
|
|
64
|
-
|
|
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.
|
|
67
|
-
uint256 totalSupplyAfter = jbTokens().totalSupplyOf(revnetId);
|
|
68
|
-
assertLe(totalSupplyAfter, totalSupplyBefore, "total supply should not increase");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/// @notice High tax rate (90%) produces small reclaim relative to pro-rata.
|
|
72
|
-
function test_fork_cashOut_highTaxRate() public {
|
|
73
|
-
// Deploy a separate revnet with 90% tax rate.
|
|
74
|
-
uint256 highTaxRevnet = _deployRevnet(9000);
|
|
75
|
-
_setupPool(highTaxRevnet, 10_000 ether);
|
|
76
|
-
_payRevnet(highTaxRevnet, PAYER, 10 ether);
|
|
77
|
-
vm.warp(block.timestamp + REV_DEPLOYER.CASH_OUT_DELAY() + 1);
|
|
78
|
-
|
|
79
|
-
uint256 payerTokens = jbTokens().totalBalanceOf(PAYER, highTaxRevnet);
|
|
80
|
-
uint256 cashOutCount = payerTokens / 2;
|
|
81
|
-
|
|
82
|
-
vm.prank(PAYER);
|
|
83
|
-
uint256 reclaimedAmount = jbMultiTerminal()
|
|
84
|
-
.cashOutTokensOf({
|
|
85
|
-
holder: PAYER,
|
|
86
|
-
projectId: highTaxRevnet,
|
|
87
|
-
cashOutCount: cashOutCount,
|
|
88
|
-
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
89
|
-
minTokensReclaimed: 0,
|
|
90
|
-
beneficiary: payable(PAYER),
|
|
91
|
-
metadata: ""
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// With 90% tax rate, reclaim should be very small relative to surplus.
|
|
95
|
-
uint256 terminalBalance = _terminalBalance(highTaxRevnet, JBConstants.NATIVE_TOKEN);
|
|
96
|
-
uint256 totalSurplus = terminalBalance + reclaimedAmount;
|
|
97
|
-
uint256 proRataShare = (totalSurplus * cashOutCount) / payerTokens;
|
|
98
|
-
|
|
99
|
-
// At 90% tax rate with 50% of supply, reclaim ~= proRata * (10% + 90% * 0.5) = proRata * 55%.
|
|
100
|
-
// But also minus 2.5% fee. Should be well under 60% of pro-rata.
|
|
101
|
-
assertLt(reclaimedAmount, (proRataShare * 60) / 100, "high tax: reclaim should be very small");
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/// @notice Sucker addresses get full pro-rata reclaim with 0% tax and no fee.
|
|
105
|
-
function test_fork_cashOut_suckerExempt() public {
|
|
106
|
-
address sucker = makeAddr("sucker");
|
|
107
|
-
vm.deal(sucker, 100 ether);
|
|
108
|
-
|
|
109
|
-
// Pay as sucker to get tokens.
|
|
110
|
-
_payRevnet(revnetId, sucker, 5 ether);
|
|
111
|
-
|
|
112
|
-
uint256 suckerTokens = jbTokens().totalBalanceOf(sucker, revnetId);
|
|
113
|
-
uint256 totalSupply = jbTokens().totalSupplyOf(revnetId);
|
|
114
|
-
uint256 surplus = _terminalBalance(revnetId, JBConstants.NATIVE_TOKEN);
|
|
115
|
-
|
|
116
|
-
// Mock sucker registry to report this address as a sucker.
|
|
117
|
-
vm.mockCall(
|
|
118
|
-
address(SUCKER_REGISTRY),
|
|
119
|
-
abi.encodeWithSelector(IJBSuckerRegistry.isSuckerOf.selector, revnetId, sucker),
|
|
120
|
-
abi.encode(true)
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
uint256 feeTerminalBefore = _terminalBalance(FEE_PROJECT_ID, JBConstants.NATIVE_TOKEN);
|
|
124
|
-
|
|
125
|
-
vm.prank(sucker);
|
|
126
|
-
uint256 reclaimedAmount = jbMultiTerminal()
|
|
127
|
-
.cashOutTokensOf({
|
|
128
|
-
holder: sucker,
|
|
129
|
-
projectId: revnetId,
|
|
130
|
-
cashOutCount: suckerTokens,
|
|
131
|
-
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
132
|
-
minTokensReclaimed: 0,
|
|
133
|
-
beneficiary: payable(sucker),
|
|
134
|
-
metadata: ""
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
// Full pro-rata reclaim (0% tax).
|
|
138
|
-
uint256 expectedReclaim = (surplus * suckerTokens) / totalSupply;
|
|
139
|
-
assertEq(reclaimedAmount, expectedReclaim, "sucker should get full pro-rata reclaim");
|
|
140
|
-
|
|
141
|
-
// No fee charged.
|
|
142
|
-
uint256 feeTerminalAfter = _terminalBalance(FEE_PROJECT_ID, JBConstants.NATIVE_TOKEN);
|
|
143
|
-
assertEq(feeTerminalAfter, feeTerminalBefore, "no fee should be charged for sucker");
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/// @notice After a payment with 30% tier split, surplus accounting reflects actual terminal balance.
|
|
147
|
-
function test_fork_cashOut_afterTierSplitPayment() public {
|
|
148
|
-
// Deploy revnet with 721 hook.
|
|
149
|
-
(uint256 splitRevnetId, IJB721TiersHook hook) = _deployRevnetWith721(5000);
|
|
150
|
-
_setupPool(splitRevnetId, 10_000 ether);
|
|
151
|
-
|
|
152
|
-
// Pay 1 ETH with tier metadata (triggers 30% split).
|
|
153
|
-
address metadataTarget = hook.METADATA_ID_TARGET();
|
|
154
|
-
bytes memory metadata = _buildPayMetadataNoQuote(metadataTarget);
|
|
155
|
-
|
|
156
|
-
vm.prank(PAYER);
|
|
157
|
-
jbMultiTerminal().pay{value: 1 ether}({
|
|
158
|
-
projectId: splitRevnetId,
|
|
159
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
160
|
-
amount: 1 ether,
|
|
161
|
-
beneficiary: PAYER,
|
|
162
|
-
minReturnedTokens: 0,
|
|
163
|
-
memo: "",
|
|
164
|
-
metadata: metadata
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
// Warp past delay.
|
|
168
|
-
vm.warp(block.timestamp + REV_DEPLOYER.CASH_OUT_DELAY() + 1);
|
|
169
|
-
|
|
170
|
-
uint256 payerTokens = jbTokens().totalBalanceOf(PAYER, splitRevnetId);
|
|
171
|
-
uint256 terminalBalance = _terminalBalance(splitRevnetId, JBConstants.NATIVE_TOKEN);
|
|
172
|
-
|
|
173
|
-
// Terminal balance should be ~1 ETH (0.7 ETH project share + 0.3 ETH returned via addToBalance from 721
|
|
174
|
-
// hook).
|
|
175
|
-
assertGt(terminalBalance, 0, "terminal should have balance");
|
|
176
|
-
|
|
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.
|
|
180
|
-
if (payerTokens > 0) {
|
|
181
|
-
uint256 payerEthBefore = PAYER.balance;
|
|
182
|
-
|
|
183
|
-
vm.prank(PAYER);
|
|
184
|
-
jbMultiTerminal()
|
|
185
|
-
.cashOutTokensOf({
|
|
186
|
-
holder: PAYER,
|
|
187
|
-
projectId: splitRevnetId,
|
|
188
|
-
cashOutCount: payerTokens,
|
|
189
|
-
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
190
|
-
minTokensReclaimed: 0,
|
|
191
|
-
beneficiary: payable(PAYER),
|
|
192
|
-
metadata: ""
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
assertGt(PAYER.balance, payerEthBefore, "payer should receive ETH after tier split cashout");
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/// @notice Cash out before delay expires should revert.
|
|
200
|
-
function test_fork_cashOut_delayEnforcement() public {
|
|
201
|
-
// Deploy a revnet whose first stage started in the past → triggers cash-out delay.
|
|
202
|
-
(REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc) =
|
|
203
|
-
_buildMinimalConfig(5000);
|
|
204
|
-
cfg.stageConfigurations[0].startsAtOrAfter = uint40(block.timestamp - 1);
|
|
205
|
-
(uint256 delayRevnet,) = REV_DEPLOYER.deployFor({
|
|
206
|
-
revnetId: 0,
|
|
207
|
-
configuration: cfg,
|
|
208
|
-
terminalConfigurations: tc,
|
|
209
|
-
suckerDeploymentConfiguration: sdc,
|
|
210
|
-
tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
|
|
211
|
-
allowedPosts: REVEmpty721Config.emptyAllowedPosts()
|
|
212
|
-
});
|
|
213
|
-
_setupPool(delayRevnet, 10_000 ether);
|
|
214
|
-
_payRevnet(delayRevnet, PAYER, 1 ether);
|
|
215
|
-
|
|
216
|
-
uint256 payerTokens = jbTokens().totalBalanceOf(PAYER, delayRevnet);
|
|
217
|
-
|
|
218
|
-
// Try to cash out immediately (before delay expires) -> should revert.
|
|
219
|
-
vm.prank(PAYER);
|
|
220
|
-
vm.expectRevert();
|
|
221
|
-
jbMultiTerminal()
|
|
222
|
-
.cashOutTokensOf({
|
|
223
|
-
holder: PAYER,
|
|
224
|
-
projectId: delayRevnet,
|
|
225
|
-
cashOutCount: payerTokens,
|
|
226
|
-
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
227
|
-
minTokensReclaimed: 0,
|
|
228
|
-
beneficiary: payable(PAYER),
|
|
229
|
-
metadata: ""
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
// Warp past delay.
|
|
233
|
-
vm.warp(block.timestamp + REV_DEPLOYER.CASH_OUT_DELAY() + 1);
|
|
234
|
-
|
|
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
|
-
|
|
239
|
-
vm.prank(PAYER);
|
|
240
|
-
jbMultiTerminal()
|
|
241
|
-
.cashOutTokensOf({
|
|
242
|
-
holder: PAYER,
|
|
243
|
-
projectId: delayRevnet,
|
|
244
|
-
cashOutCount: payerTokens,
|
|
245
|
-
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
246
|
-
minTokensReclaimed: 0,
|
|
247
|
-
beneficiary: payable(PAYER),
|
|
248
|
-
metadata: ""
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
assertGt(PAYER.balance, payerEthBefore, "should succeed after delay expires");
|
|
252
|
-
}
|
|
253
|
-
}
|
|
@@ -1,158 +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 "./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
|
-
}
|