@rev-net/core-v6 0.0.7 → 0.0.9
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 +186 -0
- package/ARCHITECTURE.md +87 -0
- package/README.md +4 -2
- package/RISKS.md +49 -0
- package/SKILLS.md +22 -2
- package/STYLE_GUIDE.md +482 -0
- package/foundry.toml +6 -6
- package/package.json +13 -10
- package/script/Deploy.s.sol +3 -2
- package/src/REVDeployer.sol +129 -72
- package/src/REVLoans.sol +174 -165
- package/src/interfaces/IREVDeployer.sol +111 -72
- package/src/interfaces/IREVLoans.sol +116 -76
- package/src/structs/REV721TiersHookFlags.sol +14 -0
- package/src/structs/REVBaseline721HookConfig.sol +27 -0
- package/src/structs/REVDeploy721TiersHookConfig.sol +2 -2
- package/test/REV.integrations.t.sol +4 -3
- package/test/REVAutoIssuanceFuzz.t.sol +12 -8
- package/test/REVDeployerAuditRegressions.t.sol +4 -3
- package/test/REVInvincibility.t.sol +8 -6
- package/test/REVInvincibilityHandler.sol +1 -0
- package/test/REVLifecycle.t.sol +4 -3
- package/test/REVLoans.invariants.t.sol +5 -3
- package/test/REVLoansAttacks.t.sol +4 -3
- package/test/REVLoansAuditRegressions.t.sol +13 -24
- package/test/REVLoansFeeRecovery.t.sol +4 -3
- package/test/REVLoansSourced.t.sol +4 -3
- package/test/REVLoansUnSourced.t.sol +4 -3
- package/test/REVLoans_AuditFindings.t.sol +644 -0
- package/test/TestEmptyBuybackSpecs.t.sol +4 -3
- package/test/TestPR09_ConversionDocumentation.t.sol +4 -3
- package/test/TestPR10_LiquidationBehavior.t.sol +4 -3
- package/test/TestPR11_LowFindings.t.sol +4 -3
- package/test/TestPR12_FlashLoanSurplus.t.sol +4 -3
- package/test/TestPR13_CrossSourceReallocation.t.sol +4 -3
- package/test/TestPR15_CashOutCallerValidation.t.sol +4 -3
- package/test/TestPR16_ZeroRepayment.t.sol +4 -3
- package/test/TestPR21_Uint112Overflow.t.sol +4 -3
- package/test/TestPR22_HookArrayOOB.t.sol +4 -3
- package/test/TestPR26_BurnHeldTokens.t.sol +4 -3
- package/test/TestPR27_CEIPattern.t.sol +4 -3
- package/test/TestPR29_SwapTerminalPermission.t.sol +4 -3
- package/test/TestPR32_MixedFixes.t.sol +4 -3
- package/test/TestSplitWeightAdjustment.t.sol +445 -0
- package/test/TestSplitWeightE2E.t.sol +528 -0
- package/test/TestSplitWeightFork.t.sol +821 -0
- package/test/TestStageTransitionBorrowable.t.sol +4 -3
- package/test/fork/ForkTestBase.sol +617 -0
- package/test/fork/TestCashOutFork.t.sol +245 -0
- package/test/fork/TestLoanBorrowFork.t.sol +163 -0
- package/test/fork/TestLoanLiquidationFork.t.sol +129 -0
- package/test/fork/TestLoanReallocateFork.t.sol +103 -0
- package/test/fork/TestLoanRepayFork.t.sol +184 -0
- package/test/fork/TestSplitWeightFork.t.sol +186 -0
- package/test/mock/MockBuybackDataHook.sol +11 -4
- package/test/mock/MockBuybackDataHookMintPath.sol +11 -3
- package/test/regression/TestI20_CumulativeLoanCounter.t.sol +6 -5
- package/test/regression/TestL27_LiquidateGapHandling.t.sol +7 -6
- package/SECURITY.md +0 -68
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
import "forge-std/Test.sol";
|
|
5
|
+
import /* {*} from */ "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
|
|
6
|
+
import /* {*} from */ "./../src/REVDeployer.sol";
|
|
7
|
+
import "@croptop/core-v6/src/CTPublisher.sol";
|
|
8
|
+
import {MockBuybackDataHookMintPath} from "./mock/MockBuybackDataHookMintPath.sol";
|
|
9
|
+
import {MockBuybackDataHook} from "./mock/MockBuybackDataHook.sol";
|
|
10
|
+
import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
|
|
11
|
+
import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
|
|
12
|
+
import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
|
|
13
|
+
import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
|
|
14
|
+
import "@bananapus/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
|
|
15
|
+
|
|
16
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
17
|
+
import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
|
|
18
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
19
|
+
import {REVLoans} from "../src/REVLoans.sol";
|
|
20
|
+
import {REVStageConfig, REVAutoIssuance} from "../src/structs/REVStageConfig.sol";
|
|
21
|
+
import {REVLoanSource} from "../src/structs/REVLoanSource.sol";
|
|
22
|
+
import {REVDescription} from "../src/structs/REVDescription.sol";
|
|
23
|
+
import {IREVLoans} from "./../src/interfaces/IREVLoans.sol";
|
|
24
|
+
import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
|
|
25
|
+
import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
|
|
26
|
+
import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
|
|
27
|
+
import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
|
|
28
|
+
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
29
|
+
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
30
|
+
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
31
|
+
import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
|
|
32
|
+
import {IJBBuybackHookRegistry} from "@bananapus/buyback-hook-v6/src/interfaces/IJBBuybackHookRegistry.sol";
|
|
33
|
+
import {IJBPayHook} from "@bananapus/core-v6/src/interfaces/IJBPayHook.sol";
|
|
34
|
+
import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
|
|
35
|
+
import {JB721InitTiersConfig} from "@bananapus/721-hook-v6/src/structs/JB721InitTiersConfig.sol";
|
|
36
|
+
import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
|
|
37
|
+
import {REVDeploy721TiersHookConfig} from "../src/structs/REVDeploy721TiersHookConfig.sol";
|
|
38
|
+
import {REVBaseline721HookConfig} from "../src/structs/REVBaseline721HookConfig.sol";
|
|
39
|
+
import {REV721TiersHookFlags} from "../src/structs/REV721TiersHookFlags.sol";
|
|
40
|
+
import {REVCroptopAllowedPost} from "../src/structs/REVCroptopAllowedPost.sol";
|
|
41
|
+
|
|
42
|
+
/// @notice E2E tests verifying that the split weight adjustment in REVDeployer produces correct token counts
|
|
43
|
+
/// when payments flow through the full terminal → store → dataHook → mint pipeline.
|
|
44
|
+
/// Tests both mint path (buyback decides to mint) and AMM path (buyback decides to swap).
|
|
45
|
+
contract TestSplitWeightE2E is TestBaseWorkflow {
|
|
46
|
+
using JBMetadataResolver for bytes;
|
|
47
|
+
|
|
48
|
+
bytes32 REV_DEPLOYER_SALT = "REVDeployer_E2E";
|
|
49
|
+
|
|
50
|
+
REVDeployer REV_DEPLOYER;
|
|
51
|
+
JB721TiersHook EXAMPLE_HOOK;
|
|
52
|
+
IJB721TiersHookDeployer HOOK_DEPLOYER;
|
|
53
|
+
IJB721TiersHookStore HOOK_STORE;
|
|
54
|
+
IJBAddressRegistry ADDRESS_REGISTRY;
|
|
55
|
+
IREVLoans LOANS_CONTRACT;
|
|
56
|
+
IJBSuckerRegistry SUCKER_REGISTRY;
|
|
57
|
+
CTPublisher PUBLISHER;
|
|
58
|
+
MockBuybackDataHookMintPath MOCK_BUYBACK_MINT;
|
|
59
|
+
|
|
60
|
+
uint256 FEE_PROJECT_ID;
|
|
61
|
+
|
|
62
|
+
address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
|
|
63
|
+
address PAYER = makeAddr("payer");
|
|
64
|
+
address SPLIT_BENEFICIARY = makeAddr("splitBeneficiary");
|
|
65
|
+
|
|
66
|
+
// Tier configuration: 1 ETH tier with 30% split.
|
|
67
|
+
uint104 constant TIER_PRICE = 1 ether;
|
|
68
|
+
uint32 constant SPLIT_PERCENT = 300_000_000; // 30% of SPLITS_TOTAL_PERCENT (1_000_000_000)
|
|
69
|
+
uint112 constant INITIAL_ISSUANCE = 1000e18; // 1000 tokens per ETH
|
|
70
|
+
|
|
71
|
+
function setUp() public override {
|
|
72
|
+
super.setUp();
|
|
73
|
+
|
|
74
|
+
FEE_PROJECT_ID = jbProjects().createFor(multisig());
|
|
75
|
+
|
|
76
|
+
SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
|
|
77
|
+
HOOK_STORE = new JB721TiersHookStore();
|
|
78
|
+
EXAMPLE_HOOK =
|
|
79
|
+
new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, jbSplits(), multisig());
|
|
80
|
+
ADDRESS_REGISTRY = new JBAddressRegistry();
|
|
81
|
+
HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
|
|
82
|
+
PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
|
|
83
|
+
MOCK_BUYBACK_MINT = new MockBuybackDataHookMintPath();
|
|
84
|
+
|
|
85
|
+
LOANS_CONTRACT = new REVLoans({
|
|
86
|
+
controller: jbController(),
|
|
87
|
+
projects: jbProjects(),
|
|
88
|
+
revId: FEE_PROJECT_ID,
|
|
89
|
+
owner: address(this),
|
|
90
|
+
permit2: permit2(),
|
|
91
|
+
trustedForwarder: TRUSTED_FORWARDER
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
|
|
95
|
+
jbController(),
|
|
96
|
+
SUCKER_REGISTRY,
|
|
97
|
+
FEE_PROJECT_ID,
|
|
98
|
+
HOOK_DEPLOYER,
|
|
99
|
+
PUBLISHER,
|
|
100
|
+
IJBBuybackHookRegistry(address(MOCK_BUYBACK_MINT)),
|
|
101
|
+
address(LOANS_CONTRACT),
|
|
102
|
+
TRUSTED_FORWARDER
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
vm.prank(multisig());
|
|
106
|
+
jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
|
|
107
|
+
|
|
108
|
+
// Fund the payer.
|
|
109
|
+
vm.deal(PAYER, 100 ether);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ───────────────────────── Helpers
|
|
113
|
+
// ─────────────────────────
|
|
114
|
+
|
|
115
|
+
function _buildMinimalConfig()
|
|
116
|
+
internal
|
|
117
|
+
view
|
|
118
|
+
returns (REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc)
|
|
119
|
+
{
|
|
120
|
+
JBAccountingContext[] memory acc = new JBAccountingContext[](1);
|
|
121
|
+
acc[0] = JBAccountingContext({
|
|
122
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
123
|
+
});
|
|
124
|
+
tc = new JBTerminalConfig[](1);
|
|
125
|
+
tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
|
|
126
|
+
|
|
127
|
+
REVStageConfig[] memory stages = new REVStageConfig[](1);
|
|
128
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
129
|
+
splits[0].beneficiary = payable(multisig());
|
|
130
|
+
splits[0].percent = 10_000;
|
|
131
|
+
stages[0] = REVStageConfig({
|
|
132
|
+
startsAtOrAfter: uint40(block.timestamp),
|
|
133
|
+
autoIssuances: new REVAutoIssuance[](0),
|
|
134
|
+
splitPercent: 0,
|
|
135
|
+
splits: splits,
|
|
136
|
+
initialIssuance: INITIAL_ISSUANCE,
|
|
137
|
+
issuanceCutFrequency: 0,
|
|
138
|
+
issuanceCutPercent: 0,
|
|
139
|
+
cashOutTaxRate: 5000,
|
|
140
|
+
extraMetadata: 0
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
cfg = REVConfig({
|
|
144
|
+
description: REVDescription("E2E Test", "E2E", "ipfs://e2e", "E2E_SALT"),
|
|
145
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
146
|
+
splitOperator: multisig(),
|
|
147
|
+
stageConfigurations: stages
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
sdc = REVSuckerDeploymentConfig({
|
|
151
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("E2E_TEST"))
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function _build721Config() internal view returns (REVDeploy721TiersHookConfig memory) {
|
|
156
|
+
// Create a tier: 1 ETH, 30% split to SPLIT_BENEFICIARY.
|
|
157
|
+
JB721TierConfig[] memory tiers = new JB721TierConfig[](1);
|
|
158
|
+
JBSplit[] memory tierSplits = new JBSplit[](1);
|
|
159
|
+
tierSplits[0] = JBSplit({
|
|
160
|
+
preferAddToBalance: false,
|
|
161
|
+
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT), // 100% of the split portion goes to this beneficiary
|
|
162
|
+
projectId: 0,
|
|
163
|
+
beneficiary: payable(SPLIT_BENEFICIARY),
|
|
164
|
+
lockedUntil: 0,
|
|
165
|
+
hook: IJBSplitHook(address(0))
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
tiers[0] = JB721TierConfig({
|
|
169
|
+
price: TIER_PRICE,
|
|
170
|
+
initialSupply: 100,
|
|
171
|
+
votingUnits: 0,
|
|
172
|
+
reserveFrequency: 0,
|
|
173
|
+
reserveBeneficiary: address(0),
|
|
174
|
+
encodedIPFSUri: bytes32("tier1"),
|
|
175
|
+
category: 1,
|
|
176
|
+
discountPercent: 0,
|
|
177
|
+
allowOwnerMint: false,
|
|
178
|
+
useReserveBeneficiaryAsDefault: false,
|
|
179
|
+
transfersPausable: false,
|
|
180
|
+
useVotingUnits: false,
|
|
181
|
+
cannotBeRemoved: false,
|
|
182
|
+
cannotIncreaseDiscountPercent: false,
|
|
183
|
+
splitPercent: SPLIT_PERCENT,
|
|
184
|
+
splits: tierSplits
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return REVDeploy721TiersHookConfig({
|
|
188
|
+
baseline721HookConfiguration: REVBaseline721HookConfig({
|
|
189
|
+
name: "E2E NFT",
|
|
190
|
+
symbol: "E2ENFT",
|
|
191
|
+
baseUri: "ipfs://",
|
|
192
|
+
tokenUriResolver: IJB721TokenUriResolver(address(0)),
|
|
193
|
+
contractUri: "ipfs://contract",
|
|
194
|
+
tiersConfig: JB721InitTiersConfig({
|
|
195
|
+
tiers: tiers,
|
|
196
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
197
|
+
decimals: 18,
|
|
198
|
+
prices: IJBPrices(address(0))
|
|
199
|
+
}),
|
|
200
|
+
reserveBeneficiary: address(0),
|
|
201
|
+
flags: REV721TiersHookFlags({
|
|
202
|
+
noNewTiersWithReserves: false,
|
|
203
|
+
noNewTiersWithVotes: false,
|
|
204
|
+
noNewTiersWithOwnerMinting: false,
|
|
205
|
+
preventOverspending: false
|
|
206
|
+
})
|
|
207
|
+
}),
|
|
208
|
+
salt: bytes32("E2E_721"),
|
|
209
|
+
splitOperatorCanAdjustTiers: false,
|
|
210
|
+
splitOperatorCanUpdateMetadata: false,
|
|
211
|
+
splitOperatorCanMint: false,
|
|
212
|
+
splitOperatorCanIncreaseDiscountPercent: false
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/// @notice Deploy the fee project, then deploy a revnet with 721 tiers.
|
|
217
|
+
function _deployRevnetWith721() internal returns (uint256 revnetId, IJB721TiersHook hook) {
|
|
218
|
+
// Deploy fee project first.
|
|
219
|
+
(REVConfig memory feeCfg, JBTerminalConfig[] memory feeTc, REVSuckerDeploymentConfig memory feeSdc) =
|
|
220
|
+
_buildMinimalConfig();
|
|
221
|
+
feeCfg.description = REVDescription("Fee", "FEE", "ipfs://fee", "FEE_SALT");
|
|
222
|
+
|
|
223
|
+
vm.prank(multisig());
|
|
224
|
+
REV_DEPLOYER.deployFor({
|
|
225
|
+
revnetId: FEE_PROJECT_ID,
|
|
226
|
+
configuration: feeCfg,
|
|
227
|
+
terminalConfigurations: feeTc,
|
|
228
|
+
suckerDeploymentConfiguration: feeSdc
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Deploy the revnet with 721 hook.
|
|
232
|
+
(REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc) =
|
|
233
|
+
_buildMinimalConfig();
|
|
234
|
+
REVDeploy721TiersHookConfig memory hookConfig = _build721Config();
|
|
235
|
+
|
|
236
|
+
(revnetId, hook) = REV_DEPLOYER.deployWith721sFor({
|
|
237
|
+
revnetId: 0,
|
|
238
|
+
configuration: cfg,
|
|
239
|
+
terminalConfigurations: tc,
|
|
240
|
+
suckerDeploymentConfiguration: sdc,
|
|
241
|
+
tiered721HookConfiguration: hookConfig,
|
|
242
|
+
allowedPosts: new REVCroptopAllowedPost[](0)
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/// @notice Build payment metadata that tells the 721 hook to mint from tier 1.
|
|
247
|
+
function _buildPayMetadata(address hookAddress) internal pure returns (bytes memory) {
|
|
248
|
+
// The 721 hook uses getId("pay", METADATA_ID_TARGET).
|
|
249
|
+
// For clones, METADATA_ID_TARGET = address(implementation), but for our test the hook
|
|
250
|
+
// is deployed via deployer and METADATA_ID_TARGET is set in the constructor.
|
|
251
|
+
// We'll use the hook address as target since that's what `address(this)` resolves to
|
|
252
|
+
// in a clone's delegatecall context... Actually for the JB721TiersHookDeployer,
|
|
253
|
+
// METADATA_ID_TARGET is the implementation address. Let's compute it directly.
|
|
254
|
+
|
|
255
|
+
// Actually, we need to read METADATA_ID_TARGET from the deployed hook.
|
|
256
|
+
// For now, let's compute the metadata ID using the hook's METADATA_ID_TARGET.
|
|
257
|
+
// We'll handle this in the test function where we have access to the hook instance.
|
|
258
|
+
|
|
259
|
+
// Tier IDs to mint: [1] (first tier is ID 1)
|
|
260
|
+
uint16[] memory tierIds = new uint16[](1);
|
|
261
|
+
tierIds[0] = 1;
|
|
262
|
+
|
|
263
|
+
// Encode: (allowOverspending, tierIdsToMint)
|
|
264
|
+
bytes memory tierData = abi.encode(true, tierIds);
|
|
265
|
+
|
|
266
|
+
// Build the metadata ID.
|
|
267
|
+
bytes4 metadataId = JBMetadataResolver.getId("pay", hookAddress);
|
|
268
|
+
|
|
269
|
+
// Build full metadata using createMetadata.
|
|
270
|
+
bytes4[] memory ids = new bytes4[](1);
|
|
271
|
+
ids[0] = metadataId;
|
|
272
|
+
bytes[] memory datas = new bytes[](1);
|
|
273
|
+
datas[0] = tierData;
|
|
274
|
+
|
|
275
|
+
return JBMetadataResolver.createMetadata(ids, datas);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ───────────────────────── Tests
|
|
279
|
+
// ─────────────────────────
|
|
280
|
+
|
|
281
|
+
/// @notice Mint path: pay 1 ETH for tier with 30% split.
|
|
282
|
+
/// Verifies tokens minted == 700 (not 1000), confirming the weight scaling is correct.
|
|
283
|
+
///
|
|
284
|
+
/// The core question this settles: does the terminal mint tokens based on the FULL payment amount?
|
|
285
|
+
/// YES — JBTerminalStore.recordPaymentFrom line 410: tokenCount = mulDiv(amount.value, weight, weightRatio)
|
|
286
|
+
/// where amount.value is the full 1 ETH, NOT the reduced 0.7 ETH.
|
|
287
|
+
///
|
|
288
|
+
/// So the weight MUST be scaled down. Without scaling:
|
|
289
|
+
/// tokenCount = mulDiv(1e18, 1000e18, 1e18) = 1000e18 → 1000 tokens for 0.7 ETH of actual value = WRONG
|
|
290
|
+
/// With scaling:
|
|
291
|
+
/// tokenCount = mulDiv(1e18, 700e18, 1e18) = 700e18 → 700 tokens for 0.7 ETH of actual value = CORRECT
|
|
292
|
+
function test_e2e_mintPath_splitReducesTokens() public {
|
|
293
|
+
(uint256 revnetId, IJB721TiersHook hook) = _deployRevnetWith721();
|
|
294
|
+
|
|
295
|
+
// Build metadata targeting the hook's METADATA_ID_TARGET.
|
|
296
|
+
address metadataTarget = hook.METADATA_ID_TARGET();
|
|
297
|
+
bytes memory metadata = _buildPayMetadata(metadataTarget);
|
|
298
|
+
|
|
299
|
+
// Pay 1 ETH through the terminal.
|
|
300
|
+
vm.prank(PAYER);
|
|
301
|
+
uint256 tokensReceived = jbMultiTerminal().pay{value: 1 ether}({
|
|
302
|
+
projectId: revnetId,
|
|
303
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
304
|
+
amount: 1 ether,
|
|
305
|
+
beneficiary: PAYER,
|
|
306
|
+
minReturnedTokens: 0,
|
|
307
|
+
memo: "E2E split weight test",
|
|
308
|
+
metadata: metadata
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Expected: 1 ETH payment, 0.3 ETH split (30% of 1 ETH tier price).
|
|
312
|
+
// projectAmount = 0.7 ETH.
|
|
313
|
+
// Buyback hook (mint path) returns context.weight unchanged.
|
|
314
|
+
// REVDeployer scales: weight = 1000e18 * 0.7e18 / 1e18 = 700e18.
|
|
315
|
+
// Terminal mints: mulDiv(1e18, 700e18, 1e18) = 700e18 = 700 tokens.
|
|
316
|
+
uint256 expectedTokens = 700e18;
|
|
317
|
+
|
|
318
|
+
assertEq(tokensReceived, expectedTokens, "tokens should be 700 (weight scaled for 30% split)");
|
|
319
|
+
|
|
320
|
+
// Confirm payer's actual token balance matches.
|
|
321
|
+
uint256 payerBalance = jbTokens().totalBalanceOf(PAYER, revnetId);
|
|
322
|
+
assertEq(payerBalance, expectedTokens, "payer balance matches expected tokens");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/// @notice Mint path without splits: pay 1 ETH with no tier metadata.
|
|
326
|
+
/// Baseline: all tokens should be minted at full weight.
|
|
327
|
+
function test_e2e_mintPath_noSplits_fullTokens() public {
|
|
328
|
+
(uint256 revnetId,) = _deployRevnetWith721();
|
|
329
|
+
|
|
330
|
+
// Pay 1 ETH with NO tier metadata (no NFT purchase, no splits).
|
|
331
|
+
vm.prank(PAYER);
|
|
332
|
+
uint256 tokensReceived = jbMultiTerminal().pay{value: 1 ether}({
|
|
333
|
+
projectId: revnetId,
|
|
334
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
335
|
+
amount: 1 ether,
|
|
336
|
+
beneficiary: PAYER,
|
|
337
|
+
minReturnedTokens: 0,
|
|
338
|
+
memo: "E2E no split test",
|
|
339
|
+
metadata: ""
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// No splits → no weight reduction. Full 1000 tokens.
|
|
343
|
+
uint256 expectedTokens = 1000e18;
|
|
344
|
+
assertEq(tokensReceived, expectedTokens, "tokens should be 1000 (no splits, full weight)");
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/// @notice Mint path: pay 2 ETH for 1 ETH tier with 30% split.
|
|
348
|
+
/// 0.3 ETH goes to split, 1.7 ETH enters project.
|
|
349
|
+
/// Weight should be scaled to 1.7/2.0 of the original.
|
|
350
|
+
function test_e2e_mintPath_overpay_splitReducesTokens() public {
|
|
351
|
+
(uint256 revnetId, IJB721TiersHook hook) = _deployRevnetWith721();
|
|
352
|
+
|
|
353
|
+
address metadataTarget = hook.METADATA_ID_TARGET();
|
|
354
|
+
bytes memory metadata = _buildPayMetadata(metadataTarget);
|
|
355
|
+
|
|
356
|
+
vm.prank(PAYER);
|
|
357
|
+
uint256 tokensReceived = jbMultiTerminal().pay{value: 2 ether}({
|
|
358
|
+
projectId: revnetId,
|
|
359
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
360
|
+
amount: 2 ether,
|
|
361
|
+
beneficiary: PAYER,
|
|
362
|
+
minReturnedTokens: 0,
|
|
363
|
+
memo: "E2E overpay split test",
|
|
364
|
+
metadata: metadata
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// 2 ETH payment, 1 tier at 1 ETH with 30% split → 0.3 ETH split.
|
|
368
|
+
// projectAmount = 2 - 0.3 = 1.7 ETH.
|
|
369
|
+
// weight = 1000e18 * 1.7 / 2.0 = 850e18.
|
|
370
|
+
// tokenCount = mulDiv(2e18, 850e18, 1e18) = 1700e18 = 1700 tokens.
|
|
371
|
+
uint256 expectedTokens = 1700e18;
|
|
372
|
+
assertEq(tokensReceived, expectedTokens, "tokens should be 1700 (weight scaled for 0.3 ETH split on 2 ETH)");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/// @notice AMM path: buyback hook returns weight=0 (swapping).
|
|
376
|
+
/// With splits, weight should still be 0 (no tokens minted by terminal).
|
|
377
|
+
function test_e2e_ammPath_splitWithBuyback_zeroWeight() public {
|
|
378
|
+
// Deploy a separate REVDeployer with the AMM buyback mock.
|
|
379
|
+
MockBuybackDataHook ammBuyback = new MockBuybackDataHook();
|
|
380
|
+
|
|
381
|
+
vm.prank(multisig());
|
|
382
|
+
jbProjects().approve(address(0), FEE_PROJECT_ID); // Clear old approval.
|
|
383
|
+
|
|
384
|
+
REVDeployer ammDeployer = new REVDeployer{salt: "REVDeployer_AMM_E2E"}(
|
|
385
|
+
jbController(),
|
|
386
|
+
SUCKER_REGISTRY,
|
|
387
|
+
FEE_PROJECT_ID,
|
|
388
|
+
HOOK_DEPLOYER,
|
|
389
|
+
PUBLISHER,
|
|
390
|
+
IJBBuybackHookRegistry(address(ammBuyback)),
|
|
391
|
+
address(LOANS_CONTRACT),
|
|
392
|
+
TRUSTED_FORWARDER
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
vm.prank(multisig());
|
|
396
|
+
jbProjects().approve(address(ammDeployer), FEE_PROJECT_ID);
|
|
397
|
+
|
|
398
|
+
// Deploy fee project.
|
|
399
|
+
(REVConfig memory feeCfg, JBTerminalConfig[] memory feeTc, REVSuckerDeploymentConfig memory feeSdc) =
|
|
400
|
+
_buildMinimalConfig();
|
|
401
|
+
feeCfg.description = REVDescription("Fee AMM", "FEEA", "ipfs://feeamm", "FEEA_SALT");
|
|
402
|
+
|
|
403
|
+
vm.prank(multisig());
|
|
404
|
+
ammDeployer.deployFor({
|
|
405
|
+
revnetId: FEE_PROJECT_ID,
|
|
406
|
+
configuration: feeCfg,
|
|
407
|
+
terminalConfigurations: feeTc,
|
|
408
|
+
suckerDeploymentConfiguration: feeSdc
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Deploy revnet with 721 hook.
|
|
412
|
+
(REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc) =
|
|
413
|
+
_buildMinimalConfig();
|
|
414
|
+
cfg.description = REVDescription("AMM E2E", "AMME", "ipfs://amme2e", "AMME_SALT");
|
|
415
|
+
REVDeploy721TiersHookConfig memory hookConfig = _build721Config();
|
|
416
|
+
|
|
417
|
+
(uint256 revnetId, IJB721TiersHook hook) = ammDeployer.deployWith721sFor({
|
|
418
|
+
revnetId: 0,
|
|
419
|
+
configuration: cfg,
|
|
420
|
+
terminalConfigurations: tc,
|
|
421
|
+
suckerDeploymentConfiguration: sdc,
|
|
422
|
+
tiered721HookConfiguration: hookConfig,
|
|
423
|
+
allowedPosts: new REVCroptopAllowedPost[](0)
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// Build metadata.
|
|
427
|
+
address metadataTarget = hook.METADATA_ID_TARGET();
|
|
428
|
+
bytes memory metadata = _buildPayMetadata(metadataTarget);
|
|
429
|
+
|
|
430
|
+
// The AMM mock buyback returns weight=context.weight + a hook spec (simulates swap decision).
|
|
431
|
+
// But wait — when projectAmount < context.amount.value, REVDeployer scales the weight.
|
|
432
|
+
// The AMM mock doesn't return weight=0 like the real buyback would for a swap.
|
|
433
|
+
// It returns context.weight with a hook spec.
|
|
434
|
+
//
|
|
435
|
+
// For the real buyback in swap mode:
|
|
436
|
+
// - Returns weight=0 (line 279 of JBBuybackHook.sol)
|
|
437
|
+
// - Returns hookSpec with amountToSwapWith
|
|
438
|
+
// - Terminal mints 0 tokens (weight=0)
|
|
439
|
+
// - Buyback hook's afterPay handles the swap and mints tokens directly via controller
|
|
440
|
+
//
|
|
441
|
+
// The mock doesn't replicate this behavior exactly.
|
|
442
|
+
// Let's verify the mock's behavior: it returns context.weight + a spec with amount=0.
|
|
443
|
+
// So this test really shows the mint path with an extra hook spec, not true AMM.
|
|
444
|
+
//
|
|
445
|
+
// For a true AMM test we'd need a real Uniswap pool. For now, verify that
|
|
446
|
+
// when the buyback hook returns weight=0 (which we can mock), tokens = 0.
|
|
447
|
+
|
|
448
|
+
// Mock the buyback to return weight=0 (swap mode) for any call.
|
|
449
|
+
vm.mockCall(
|
|
450
|
+
address(ammBuyback),
|
|
451
|
+
abi.encodeWithSelector(IJBRulesetDataHook.beforePayRecordedWith.selector),
|
|
452
|
+
abi.encode(
|
|
453
|
+
uint256(0), // weight = 0 (buying back from AMM)
|
|
454
|
+
new JBPayHookSpecification[](0)
|
|
455
|
+
)
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
vm.prank(PAYER);
|
|
459
|
+
uint256 tokensReceived = jbMultiTerminal().pay{value: 1 ether}({
|
|
460
|
+
projectId: revnetId,
|
|
461
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
462
|
+
amount: 1 ether,
|
|
463
|
+
beneficiary: PAYER,
|
|
464
|
+
minReturnedTokens: 0,
|
|
465
|
+
memo: "E2E AMM + split test",
|
|
466
|
+
metadata: metadata
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// Buyback returns weight=0 → REVDeployer preserves 0 (both branches: projectAmount==0 → 0, else
|
|
470
|
+
// mulDiv(0,...) → 0). Terminal: tokenCount = mulDiv(1e18, 0, 1e18) = 0.
|
|
471
|
+
// No tokens minted by terminal. In production, the buyback hook's afterPay would handle the swap.
|
|
472
|
+
assertEq(tokensReceived, 0, "AMM path: terminal should mint 0 tokens (buyback handles swap)");
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/// @notice Verify the invariant: tokens / projectAmount is the same rate regardless of split percentage.
|
|
476
|
+
/// This proves the weight scaling keeps the token-per-ETH rate consistent.
|
|
477
|
+
///
|
|
478
|
+
/// With splits: 700 tokens for 0.7 ETH entering project = 1000 tokens/ETH
|
|
479
|
+
/// Without splits: 1000 tokens for 1.0 ETH entering project = 1000 tokens/ETH
|
|
480
|
+
function test_e2e_invariant_tokenPerEthConsistent() public {
|
|
481
|
+
// --- Revnet 1: with 721 splits (30%) ---
|
|
482
|
+
(uint256 revnetId1, IJB721TiersHook hook1) = _deployRevnetWith721();
|
|
483
|
+
address metadataTarget1 = hook1.METADATA_ID_TARGET();
|
|
484
|
+
bytes memory metadata1 = _buildPayMetadata(metadataTarget1);
|
|
485
|
+
|
|
486
|
+
vm.prank(PAYER);
|
|
487
|
+
uint256 tokens1 = jbMultiTerminal().pay{value: 1 ether}({
|
|
488
|
+
projectId: revnetId1,
|
|
489
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
490
|
+
amount: 1 ether,
|
|
491
|
+
beneficiary: PAYER,
|
|
492
|
+
minReturnedTokens: 0,
|
|
493
|
+
memo: "invariant test: with splits",
|
|
494
|
+
metadata: metadata1
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// --- Revnet 2: no splits (plain payment, no tier metadata) ---
|
|
498
|
+
(REVConfig memory cfg2, JBTerminalConfig[] memory tc2, REVSuckerDeploymentConfig memory sdc2) =
|
|
499
|
+
_buildMinimalConfig();
|
|
500
|
+
cfg2.description = REVDescription("NoSplit", "NS", "ipfs://nosplit", "NOSPLIT_SALT");
|
|
501
|
+
|
|
502
|
+
uint256 revnetId2 = REV_DEPLOYER.deployFor({
|
|
503
|
+
revnetId: 0, configuration: cfg2, terminalConfigurations: tc2, suckerDeploymentConfiguration: sdc2
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
vm.prank(PAYER);
|
|
507
|
+
uint256 tokens2 = jbMultiTerminal().pay{value: 1 ether}({
|
|
508
|
+
projectId: revnetId2,
|
|
509
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
510
|
+
amount: 1 ether,
|
|
511
|
+
beneficiary: PAYER,
|
|
512
|
+
minReturnedTokens: 0,
|
|
513
|
+
memo: "invariant test: no splits",
|
|
514
|
+
metadata: ""
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// Invariant: tokens / projectAmount should produce the same rate.
|
|
518
|
+
// Revnet 1: 700e18 tokens / 0.7 ETH = 1000 tokens/ETH
|
|
519
|
+
// Revnet 2: 1000e18 tokens / 1.0 ETH = 1000 tokens/ETH
|
|
520
|
+
uint256 projectAmount1 = 0.7 ether; // 1 ETH - 30% split
|
|
521
|
+
uint256 projectAmount2 = 1 ether; // no splits
|
|
522
|
+
|
|
523
|
+
uint256 rate1 = (tokens1 * 1e18) / projectAmount1;
|
|
524
|
+
uint256 rate2 = (tokens2 * 1e18) / projectAmount2;
|
|
525
|
+
|
|
526
|
+
assertEq(rate1, rate2, "token-per-ETH rate should be identical with and without splits");
|
|
527
|
+
}
|
|
528
|
+
}
|