@rev-net/core-v6 0.0.14 → 0.0.16
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 +5 -1
- package/ARCHITECTURE.md +69 -11
- package/AUDIT_INSTRUCTIONS.md +90 -7
- package/CHANGE_LOG.md +16 -3
- package/README.md +32 -7
- package/RISKS.md +26 -14
- package/SKILLS.md +168 -46
- package/STYLE_GUIDE.md +1 -1
- package/USER_JOURNEYS.md +20 -6
- package/foundry.toml +7 -0
- package/package.json +9 -10
- package/script/Deploy.s.sol +80 -16
- package/script/helpers/RevnetCoreDeploymentLib.sol +1 -1
- package/src/REVDeployer.sol +73 -21
- package/src/REVLoans.sol +27 -6
- package/test/REV.integrations.t.sol +1 -1
- package/test/REVAutoIssuanceFuzz.t.sol +1 -1
- package/test/REVDeployerRegressions.t.sol +7 -4
- package/test/REVInvincibility.t.sol +7 -19
- package/test/REVInvincibilityHandler.sol +1 -1
- package/test/REVLifecycle.t.sol +1 -1
- package/test/REVLoans.invariants.t.sol +1 -1
- package/test/REVLoansAttacks.t.sol +20 -12
- package/test/REVLoansFeeRecovery.t.sol +20 -12
- package/test/REVLoansFindings.t.sol +20 -12
- package/test/REVLoansRegressions.t.sol +20 -12
- package/test/REVLoansSourceFeeRecovery.t.sol +1 -1
- package/test/REVLoansSourced.t.sol +1 -9
- package/test/REVLoansUnSourced.t.sol +1 -1
- package/test/TestBurnHeldTokens.t.sol +1 -1
- package/test/TestCEIPattern.t.sol +1 -1
- package/test/TestCashOutCallerValidation.t.sol +75 -1
- package/test/TestConversionDocumentation.t.sol +1 -1
- package/test/TestCrossCurrencyReclaim.t.sol +1 -1
- package/test/TestCrossSourceReallocation.t.sol +1 -1
- package/test/TestERC2771MetaTx.t.sol +1 -1
- package/test/TestEmptyBuybackSpecs.t.sol +1 -1
- package/test/TestFlashLoanSurplus.t.sol +1 -1
- package/test/TestHookArrayOOB.t.sol +1 -1
- package/test/TestLiquidationBehavior.t.sol +1 -1
- package/test/TestLoanSourceRotation.t.sol +1 -1
- package/test/TestLongTailEconomics.t.sol +1 -1
- package/test/TestLowFindings.t.sol +4 -2
- package/test/TestMixedFixes.t.sol +7 -5
- package/test/TestPermit2Signatures.t.sol +1 -1
- package/test/TestReallocationSandwich.t.sol +1 -1
- package/test/TestRevnetRegressions.t.sol +1 -1
- package/test/TestSplitWeightAdjustment.t.sol +11 -6
- package/test/TestSplitWeightE2E.t.sol +1 -1
- package/test/TestSplitWeightFork.t.sol +9 -10
- package/test/TestStageTransitionBorrowable.t.sol +1 -1
- package/test/TestSwapTerminalPermission.t.sol +1 -1
- package/test/TestUint112Overflow.t.sol +1 -1
- package/test/TestZeroRepayment.t.sol +1 -1
- package/test/audit/LoanIdOverflowGuard.t.sol +497 -0
- package/test/fork/ForkTestBase.sol +8 -11
- package/test/fork/TestAutoIssuanceFork.t.sol +148 -0
- package/test/fork/TestCashOutFork.t.sol +23 -22
- package/test/fork/TestIssuanceDecayFork.t.sol +158 -0
- package/test/fork/TestLoanBorrowFork.t.sol +1 -1
- package/test/fork/TestLoanCrossRulesetFork.t.sol +1 -1
- package/test/fork/TestLoanERC20Fork.t.sol +463 -0
- package/test/fork/TestLoanLiquidationFork.t.sol +1 -1
- package/test/fork/TestLoanReallocateFork.t.sol +1 -1
- package/test/fork/TestLoanRepayFork.t.sol +3 -3
- package/test/fork/TestLoanTransferFork.t.sol +1 -1
- package/test/fork/TestPermit2PaymentFork.t.sol +299 -0
- package/test/fork/TestSplitWeightFork.t.sol +1 -1
- package/test/helpers/MaliciousContracts.sol +37 -23
- package/test/mock/MockBuybackCashOutRecorder.sol +82 -0
- package/test/mock/MockBuybackDataHook.sol +51 -7
- package/test/mock/MockBuybackDataHookMintPath.sol +1 -1
- package/test/regression/TestBurnPermissionRequired.t.sol +1 -1
- package/test/regression/TestCashOutBuybackFeeLeak.t.sol +205 -0
- package/test/regression/TestCrossRevnetLiquidation.t.sol +1 -1
- package/test/regression/TestCumulativeLoanCounter.t.sol +1 -1
- package/test/regression/TestLiquidateGapHandling.t.sol +1 -1
- package/test/regression/TestZeroPriceFeed.t.sol +1 -1
|
@@ -0,0 +1,497 @@
|
|
|
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
|
+
// Core constants and structs used throughout the test.
|
|
24
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
25
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
26
|
+
import {JBSingleAllowance} from "@bananapus/core-v6/src/structs/JBSingleAllowance.sol";
|
|
27
|
+
// Price feed mock for native-token-to-native-token identity pricing.
|
|
28
|
+
import {MockPriceFeed} from "@bananapus/core-v6/test/mock/MockPriceFeed.sol";
|
|
29
|
+
// REVLoans contract and its supporting types.
|
|
30
|
+
import {REVLoans} from "../../src/REVLoans.sol";
|
|
31
|
+
import {REVLoan} from "../../src/structs/REVLoan.sol";
|
|
32
|
+
import {REVStageConfig, REVAutoIssuance} from "../../src/structs/REVStageConfig.sol";
|
|
33
|
+
import {REVLoanSource} from "../../src/structs/REVLoanSource.sol";
|
|
34
|
+
import {REVDescription} from "../../src/structs/REVDescription.sol";
|
|
35
|
+
// Deployment dependencies for suckers, 721 hooks, and address registry.
|
|
36
|
+
import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
|
|
37
|
+
import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
|
|
38
|
+
import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
|
|
39
|
+
import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
|
|
40
|
+
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
41
|
+
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
42
|
+
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
43
|
+
// Helper that provides empty 721 tier configs for revnet deployment.
|
|
44
|
+
import {REVEmpty721Config} from "../helpers/REVEmpty721Config.sol";
|
|
45
|
+
|
|
46
|
+
/// @notice Regression tests for the loan ID overflow guard in REVLoans.
|
|
47
|
+
/// @dev The totalLoansBorrowedFor counter must never exceed _ONE_TRILLION (1e12).
|
|
48
|
+
/// When it reaches that limit, borrowFrom, _reallocateCollateralFromLoan, and
|
|
49
|
+
/// the partial-repay branch of repayLoan must all revert with REVLoans_LoanIdOverflow().
|
|
50
|
+
/// These tests use vm.store to set the counter to the limit, then verify the revert.
|
|
51
|
+
contract LoanIdOverflowGuard is TestBaseWorkflow {
|
|
52
|
+
// ---------------------------------------------------------------
|
|
53
|
+
// Constants
|
|
54
|
+
// ---------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
/// @dev Salt for deterministic REVDeployer deployment.
|
|
57
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
58
|
+
bytes32 REV_DEPLOYER_SALT = "REVDeployer";
|
|
59
|
+
|
|
60
|
+
/// @dev The overflow boundary -- must match _ONE_TRILLION in REVLoans.sol.
|
|
61
|
+
uint256 private constant _ONE_TRILLION = 1_000_000_000_000;
|
|
62
|
+
|
|
63
|
+
/// @dev Storage slot of the totalLoansBorrowedFor mapping in REVLoans (slot 8).
|
|
64
|
+
/// Determined via `forge inspect REVLoans storage-layout`.
|
|
65
|
+
uint256 private constant TOTAL_LOANS_BORROWED_FOR_SLOT = 8;
|
|
66
|
+
|
|
67
|
+
/// @dev The address that is allowed to forward meta-transactions.
|
|
68
|
+
address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------
|
|
71
|
+
// State variables
|
|
72
|
+
// ---------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
75
|
+
REVDeployer REV_DEPLOYER;
|
|
76
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
77
|
+
JB721TiersHook EXAMPLE_HOOK;
|
|
78
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
79
|
+
IJB721TiersHookDeployer HOOK_DEPLOYER;
|
|
80
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
81
|
+
IJB721TiersHookStore HOOK_STORE;
|
|
82
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
83
|
+
IJBAddressRegistry ADDRESS_REGISTRY;
|
|
84
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
85
|
+
REVLoans LOANS_CONTRACT;
|
|
86
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
87
|
+
IJBSuckerRegistry SUCKER_REGISTRY;
|
|
88
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
89
|
+
CTPublisher PUBLISHER;
|
|
90
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
91
|
+
MockBuybackDataHook MOCK_BUYBACK;
|
|
92
|
+
|
|
93
|
+
/// @dev The fee project ID (project 1).
|
|
94
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
95
|
+
uint256 FEE_PROJECT_ID;
|
|
96
|
+
|
|
97
|
+
/// @dev The revnet project ID used by all tests.
|
|
98
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
99
|
+
uint256 REVNET_ID;
|
|
100
|
+
|
|
101
|
+
/// @dev Test user address with ETH for paying into the revnet.
|
|
102
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
103
|
+
address USER = makeAddr("user");
|
|
104
|
+
|
|
105
|
+
/// @dev Second test user used to increase revnet surplus between loan creation and reallocation.
|
|
106
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
107
|
+
address USER2 = makeAddr("user2");
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------
|
|
110
|
+
// Setup
|
|
111
|
+
// ---------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
function setUp() public override {
|
|
114
|
+
// Initialize the base test workflow (deploys core contracts).
|
|
115
|
+
super.setUp();
|
|
116
|
+
|
|
117
|
+
// Create the fee project owned by multisig.
|
|
118
|
+
FEE_PROJECT_ID = jbProjects().createFor(multisig());
|
|
119
|
+
|
|
120
|
+
// Deploy the sucker registry (no deployers, no initial suckers).
|
|
121
|
+
SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
|
|
122
|
+
|
|
123
|
+
// Deploy the 721 hook store.
|
|
124
|
+
HOOK_STORE = new JB721TiersHookStore();
|
|
125
|
+
|
|
126
|
+
// Deploy the example 721 hook (needed as the implementation for the deployer).
|
|
127
|
+
EXAMPLE_HOOK = new JB721TiersHook(
|
|
128
|
+
jbDirectory(), jbPermissions(), jbPrices(), jbRulesets(), HOOK_STORE, jbSplits(), multisig()
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// Deploy the address registry (used by the hook deployer).
|
|
132
|
+
ADDRESS_REGISTRY = new JBAddressRegistry();
|
|
133
|
+
|
|
134
|
+
// Deploy the 721 hook deployer.
|
|
135
|
+
HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
|
|
136
|
+
|
|
137
|
+
// Deploy the croptop publisher.
|
|
138
|
+
PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
|
|
139
|
+
|
|
140
|
+
// Deploy the mock buyback data hook (satisfies the IJBBuybackHookRegistry interface).
|
|
141
|
+
MOCK_BUYBACK = new MockBuybackDataHook();
|
|
142
|
+
|
|
143
|
+
// Add a 1:1 native token price feed so bonding curve math works.
|
|
144
|
+
MockPriceFeed priceFeed = new MockPriceFeed(1e18, 18);
|
|
145
|
+
vm.prank(multisig());
|
|
146
|
+
jbPrices()
|
|
147
|
+
.addPriceFeedFor(
|
|
148
|
+
0, uint32(uint160(JBConstants.NATIVE_TOKEN)), uint32(uint160(JBConstants.NATIVE_TOKEN)), priceFeed
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// Deploy the REVLoans contract.
|
|
152
|
+
LOANS_CONTRACT = new REVLoans({
|
|
153
|
+
controller: jbController(),
|
|
154
|
+
projects: jbProjects(),
|
|
155
|
+
revId: FEE_PROJECT_ID,
|
|
156
|
+
owner: address(this),
|
|
157
|
+
permit2: permit2(),
|
|
158
|
+
trustedForwarder: TRUSTED_FORWARDER
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Deploy the REVDeployer with a deterministic salt.
|
|
162
|
+
REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
|
|
163
|
+
jbController(),
|
|
164
|
+
SUCKER_REGISTRY,
|
|
165
|
+
FEE_PROJECT_ID,
|
|
166
|
+
HOOK_DEPLOYER,
|
|
167
|
+
PUBLISHER,
|
|
168
|
+
IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
|
|
169
|
+
address(LOANS_CONTRACT),
|
|
170
|
+
TRUSTED_FORWARDER
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// Approve the deployer to configure the fee project.
|
|
174
|
+
vm.prank(multisig());
|
|
175
|
+
jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
|
|
176
|
+
|
|
177
|
+
// Deploy the fee project's revnet configuration.
|
|
178
|
+
_deployFeeProject();
|
|
179
|
+
|
|
180
|
+
// Deploy the test revnet that loans will be issued against.
|
|
181
|
+
_deployRevnet();
|
|
182
|
+
|
|
183
|
+
// Give the test users 100 ETH each.
|
|
184
|
+
vm.deal(USER, 100e18);
|
|
185
|
+
vm.deal(USER2, 100e18);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------
|
|
189
|
+
// Internal helpers
|
|
190
|
+
// ---------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
/// @dev Deploys the fee project (project 1) with a single stage.
|
|
193
|
+
function _deployFeeProject() internal {
|
|
194
|
+
// Accept native token through the multi terminal.
|
|
195
|
+
JBAccountingContext[] memory acc = new JBAccountingContext[](1);
|
|
196
|
+
acc[0] = JBAccountingContext({
|
|
197
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Configure a single terminal.
|
|
201
|
+
JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
|
|
202
|
+
tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
|
|
203
|
+
|
|
204
|
+
// A single stage with auto-issuance for the multisig.
|
|
205
|
+
REVStageConfig[] memory stages = new REVStageConfig[](1);
|
|
206
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
207
|
+
splits[0].beneficiary = payable(multisig());
|
|
208
|
+
splits[0].percent = 10_000;
|
|
209
|
+
|
|
210
|
+
// Auto-issue 70k tokens to multisig on this chain.
|
|
211
|
+
REVAutoIssuance[] memory ai = new REVAutoIssuance[](1);
|
|
212
|
+
ai[0] = REVAutoIssuance({chainId: uint32(block.chainid), count: uint104(70_000e18), beneficiary: multisig()});
|
|
213
|
+
|
|
214
|
+
// Build the stage configuration.
|
|
215
|
+
stages[0] = REVStageConfig({
|
|
216
|
+
startsAtOrAfter: uint40(block.timestamp),
|
|
217
|
+
autoIssuances: ai,
|
|
218
|
+
splitPercent: 2000,
|
|
219
|
+
splits: splits,
|
|
220
|
+
initialIssuance: uint112(1000e18),
|
|
221
|
+
issuanceCutFrequency: 90 days,
|
|
222
|
+
issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
|
|
223
|
+
cashOutTaxRate: 6000,
|
|
224
|
+
extraMetadata: 0
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Build the revnet configuration for the fee project.
|
|
228
|
+
REVConfig memory cfg = REVConfig({
|
|
229
|
+
// forge-lint: disable-next-line(named-struct-fields)
|
|
230
|
+
description: REVDescription("Revnet", "$REV", "ipfs://test", "REV_TOKEN"),
|
|
231
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
232
|
+
splitOperator: multisig(),
|
|
233
|
+
stageConfigurations: stages
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Deploy the fee project revnet.
|
|
237
|
+
vm.prank(multisig());
|
|
238
|
+
REV_DEPLOYER.deployFor({
|
|
239
|
+
revnetId: FEE_PROJECT_ID,
|
|
240
|
+
configuration: cfg,
|
|
241
|
+
terminalConfigurations: tc,
|
|
242
|
+
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
243
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("FEE")
|
|
244
|
+
}),
|
|
245
|
+
tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
|
|
246
|
+
allowedPosts: REVEmpty721Config.emptyAllowedPosts()
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/// @dev Deploys the test revnet (project 2) with a single stage and 60% cash-out tax.
|
|
251
|
+
function _deployRevnet() internal {
|
|
252
|
+
// Accept native token through the multi terminal.
|
|
253
|
+
JBAccountingContext[] memory acc = new JBAccountingContext[](1);
|
|
254
|
+
acc[0] = JBAccountingContext({
|
|
255
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Configure a single terminal.
|
|
259
|
+
JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
|
|
260
|
+
tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
|
|
261
|
+
|
|
262
|
+
// A single stage with auto-issuance for the multisig.
|
|
263
|
+
REVStageConfig[] memory stages = new REVStageConfig[](1);
|
|
264
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
265
|
+
splits[0].beneficiary = payable(multisig());
|
|
266
|
+
splits[0].percent = 10_000;
|
|
267
|
+
|
|
268
|
+
// Auto-issue 70k tokens to multisig on this chain.
|
|
269
|
+
REVAutoIssuance[] memory ai = new REVAutoIssuance[](1);
|
|
270
|
+
ai[0] = REVAutoIssuance({chainId: uint32(block.chainid), count: uint104(70_000e18), beneficiary: multisig()});
|
|
271
|
+
|
|
272
|
+
// Build the stage configuration.
|
|
273
|
+
stages[0] = REVStageConfig({
|
|
274
|
+
startsAtOrAfter: uint40(block.timestamp),
|
|
275
|
+
autoIssuances: ai,
|
|
276
|
+
splitPercent: 2000,
|
|
277
|
+
splits: splits,
|
|
278
|
+
initialIssuance: uint112(1000e18),
|
|
279
|
+
issuanceCutFrequency: 90 days,
|
|
280
|
+
issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
|
|
281
|
+
cashOutTaxRate: 6000,
|
|
282
|
+
extraMetadata: 0
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Build the revnet configuration for the test project.
|
|
286
|
+
REVConfig memory cfg = REVConfig({
|
|
287
|
+
// forge-lint: disable-next-line(named-struct-fields)
|
|
288
|
+
description: REVDescription("NANA", "$NANA", "ipfs://test2", "NANA_TOKEN"),
|
|
289
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
290
|
+
splitOperator: multisig(),
|
|
291
|
+
stageConfigurations: stages
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// Deploy the test revnet (revnetId 0 means "create new").
|
|
295
|
+
(REVNET_ID,) = REV_DEPLOYER.deployFor({
|
|
296
|
+
revnetId: 0,
|
|
297
|
+
configuration: cfg,
|
|
298
|
+
terminalConfigurations: tc,
|
|
299
|
+
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
300
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("NANA")
|
|
301
|
+
}),
|
|
302
|
+
tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
|
|
303
|
+
allowedPosts: REVEmpty721Config.emptyAllowedPosts()
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/// @dev Creates a loan for the given user by paying ETH into the revnet then borrowing.
|
|
308
|
+
/// @param user The address that will own the loan.
|
|
309
|
+
/// @param ethAmount The amount of ETH to pay into the revnet as collateral.
|
|
310
|
+
/// @return loanId The ID of the created loan.
|
|
311
|
+
/// @return tokenCount The number of revnet tokens received from paying.
|
|
312
|
+
function _setupLoan(address user, uint256 ethAmount) internal returns (uint256 loanId, uint256 tokenCount) {
|
|
313
|
+
// Pay ETH into the revnet and receive tokens.
|
|
314
|
+
vm.prank(user);
|
|
315
|
+
tokenCount =
|
|
316
|
+
jbMultiTerminal().pay{value: ethAmount}(REVNET_ID, JBConstants.NATIVE_TOKEN, ethAmount, user, 0, "", "");
|
|
317
|
+
|
|
318
|
+
// Compute the borrowable amount from the tokens received.
|
|
319
|
+
uint256 borrowAmount =
|
|
320
|
+
LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokenCount, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
321
|
+
|
|
322
|
+
// Sanity check: the user should be able to borrow something.
|
|
323
|
+
require(borrowAmount > 0, "Borrow amount should be > 0");
|
|
324
|
+
|
|
325
|
+
// Mock the permissions check so LOANS_CONTRACT can burn the user's tokens.
|
|
326
|
+
mockExpect(
|
|
327
|
+
address(jbPermissions()),
|
|
328
|
+
abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), user, REVNET_ID, 11, true, true)),
|
|
329
|
+
abi.encode(true)
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
// Build the loan source pointing at the real multi terminal and native token.
|
|
333
|
+
REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
334
|
+
|
|
335
|
+
// Borrow with minimum fee percent (25 = 2.5%).
|
|
336
|
+
vm.prank(user);
|
|
337
|
+
(loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), 25);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/// @dev Computes the storage slot for totalLoansBorrowedFor[revnetId].
|
|
341
|
+
/// @param revnetId The revnet ID to compute the mapping slot for.
|
|
342
|
+
/// @return The keccak256 slot for the mapping entry.
|
|
343
|
+
function _totalLoansBorrowedSlot(uint256 revnetId) internal pure returns (bytes32) {
|
|
344
|
+
// Solidity mapping slot: keccak256(abi.encode(key, baseSlot)).
|
|
345
|
+
return keccak256(abi.encode(revnetId, TOTAL_LOANS_BORROWED_FOR_SLOT));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ---------------------------------------------------------------
|
|
349
|
+
// Test 1: borrowFrom overflow guard
|
|
350
|
+
// ---------------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
/// @notice Verifies that borrowFrom reverts with REVLoans_LoanIdOverflow when
|
|
353
|
+
/// the totalLoansBorrowedFor counter has reached _ONE_TRILLION.
|
|
354
|
+
function test_borrowFrom_revertsAtOverflowBoundary() public {
|
|
355
|
+
// Pay ETH into the revnet so the user has tokens for collateral.
|
|
356
|
+
vm.prank(USER);
|
|
357
|
+
uint256 tokens = jbMultiTerminal().pay{value: 5e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 5e18, USER, 0, "", "");
|
|
358
|
+
|
|
359
|
+
// Verify the user received tokens.
|
|
360
|
+
assertGt(tokens, 0, "user should receive tokens from paying");
|
|
361
|
+
|
|
362
|
+
// Compute the borrowable amount from the user's tokens.
|
|
363
|
+
uint256 borrowAmount =
|
|
364
|
+
LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
365
|
+
|
|
366
|
+
// Sanity check: there should be a borrowable amount.
|
|
367
|
+
assertGt(borrowAmount, 0, "borrowable amount should be > 0");
|
|
368
|
+
|
|
369
|
+
// No permission mock needed: the overflow guard fires before any permission/burn check.
|
|
370
|
+
|
|
371
|
+
// Use vm.store to set totalLoansBorrowedFor[REVNET_ID] to _ONE_TRILLION.
|
|
372
|
+
vm.store(address(LOANS_CONTRACT), _totalLoansBorrowedSlot(REVNET_ID), bytes32(_ONE_TRILLION));
|
|
373
|
+
|
|
374
|
+
// Confirm the counter is now at the overflow boundary.
|
|
375
|
+
assertEq(
|
|
376
|
+
LOANS_CONTRACT.totalLoansBorrowedFor(REVNET_ID),
|
|
377
|
+
_ONE_TRILLION,
|
|
378
|
+
"counter should be at _ONE_TRILLION after vm.store"
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
// Build the loan source pointing at the real terminal and native token.
|
|
382
|
+
REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
383
|
+
|
|
384
|
+
// Expect the overflow revert.
|
|
385
|
+
vm.expectRevert(REVLoans.REVLoans_LoanIdOverflow.selector);
|
|
386
|
+
|
|
387
|
+
// Attempt to borrow -- should revert because the counter is at the limit.
|
|
388
|
+
vm.prank(USER);
|
|
389
|
+
LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokens, payable(USER), 25);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ---------------------------------------------------------------
|
|
393
|
+
// Test 2: reallocateCollateralFromLoan overflow guard
|
|
394
|
+
// ---------------------------------------------------------------
|
|
395
|
+
|
|
396
|
+
/// @notice Verifies that reallocateCollateralFromLoan reverts with REVLoans_LoanIdOverflow
|
|
397
|
+
/// when the totalLoansBorrowedFor counter has reached _ONE_TRILLION.
|
|
398
|
+
/// @dev A second user injects surplus after the loan is created so that removing a small
|
|
399
|
+
/// amount of collateral still leaves borrowable value >= the original loan amount (otherwise
|
|
400
|
+
/// the ReallocatingMoreCollateralThanBorrowedAmountAllows check fires first).
|
|
401
|
+
function test_reallocateCollateral_revertsAtOverflowBoundary() public {
|
|
402
|
+
// Create a loan with enough collateral that we can split off some for reallocation.
|
|
403
|
+
(uint256 loanId,) = _setupLoan(USER, 10e18);
|
|
404
|
+
|
|
405
|
+
// Verify the loan was created successfully.
|
|
406
|
+
REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
|
|
407
|
+
assertGt(loan.collateral, 0, "loan should have collateral");
|
|
408
|
+
assertGt(loan.amount, 0, "loan should have a borrow amount");
|
|
409
|
+
|
|
410
|
+
// Add surplus to the revnet WITHOUT minting tokens (addToBalanceOf, not pay).
|
|
411
|
+
// This increases the per-token borrowable value so that after removing a small
|
|
412
|
+
// amount of collateral, the borrowable amount still exceeds the original loan
|
|
413
|
+
// amount (avoiding the ReallocatingMoreCollateralThanBorrowedAmountAllows check
|
|
414
|
+
// at line 1181 and reaching the overflow guard at line 1186).
|
|
415
|
+
vm.prank(USER2);
|
|
416
|
+
jbMultiTerminal().addToBalanceOf{value: 50e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 50e18, false, "", "");
|
|
417
|
+
|
|
418
|
+
// No permission mock needed: the overflow guard in _reallocateCollateralFromLoan fires
|
|
419
|
+
// before any permission/burn check is reached.
|
|
420
|
+
|
|
421
|
+
// Use vm.store to set totalLoansBorrowedFor[REVNET_ID] to _ONE_TRILLION.
|
|
422
|
+
vm.store(address(LOANS_CONTRACT), _totalLoansBorrowedSlot(REVNET_ID), bytes32(_ONE_TRILLION));
|
|
423
|
+
|
|
424
|
+
// Confirm the counter is at the overflow boundary.
|
|
425
|
+
assertEq(
|
|
426
|
+
LOANS_CONTRACT.totalLoansBorrowedFor(REVNET_ID),
|
|
427
|
+
_ONE_TRILLION,
|
|
428
|
+
"counter should be at _ONE_TRILLION after vm.store"
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
// Build the loan source matching the existing loan's source.
|
|
432
|
+
REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
433
|
+
|
|
434
|
+
// Transfer only 1 token of collateral to trigger the reallocation path.
|
|
435
|
+
uint256 collateralToTransfer = 1;
|
|
436
|
+
|
|
437
|
+
// Expect the overflow revert from _reallocateCollateralFromLoan.
|
|
438
|
+
vm.expectRevert(REVLoans.REVLoans_LoanIdOverflow.selector);
|
|
439
|
+
|
|
440
|
+
// Attempt to reallocate -- should revert because the counter is at the limit.
|
|
441
|
+
vm.prank(USER);
|
|
442
|
+
LOANS_CONTRACT.reallocateCollateralFromLoan(
|
|
443
|
+
loanId,
|
|
444
|
+
collateralToTransfer,
|
|
445
|
+
source,
|
|
446
|
+
0, // minBorrowAmount for the new loan
|
|
447
|
+
0, // no additional collateral to add
|
|
448
|
+
payable(USER),
|
|
449
|
+
25 // prepaidFeePercent (2.5%)
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ---------------------------------------------------------------
|
|
454
|
+
// Test 3: repayLoan (partial) overflow guard
|
|
455
|
+
// ---------------------------------------------------------------
|
|
456
|
+
|
|
457
|
+
/// @notice Verifies that a partial repayLoan reverts with REVLoans_LoanIdOverflow
|
|
458
|
+
/// when the totalLoansBorrowedFor counter has reached _ONE_TRILLION.
|
|
459
|
+
/// @dev A partial repayment creates a replacement loan with a new ID, which requires
|
|
460
|
+
/// incrementing the counter. If the counter is at the limit, this must revert.
|
|
461
|
+
function test_partialRepay_revertsAtOverflowBoundary() public {
|
|
462
|
+
// Create a loan for partial repayment testing.
|
|
463
|
+
(uint256 loanId,) = _setupLoan(USER, 5e18);
|
|
464
|
+
|
|
465
|
+
// Verify the loan exists and has a borrow amount.
|
|
466
|
+
REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
|
|
467
|
+
assertGt(loan.amount, 0, "loan should have a borrow amount");
|
|
468
|
+
assertGt(loan.collateral, 0, "loan should have collateral");
|
|
469
|
+
|
|
470
|
+
// Calculate a partial repayment (half the borrow amount, return no collateral).
|
|
471
|
+
uint256 halfAmount = loan.amount / 2;
|
|
472
|
+
|
|
473
|
+
// Sanity check: half amount must be non-zero for a meaningful partial repay.
|
|
474
|
+
assertGt(halfAmount, 0, "half amount should be > 0");
|
|
475
|
+
|
|
476
|
+
// Use vm.store to set totalLoansBorrowedFor[REVNET_ID] to _ONE_TRILLION.
|
|
477
|
+
vm.store(address(LOANS_CONTRACT), _totalLoansBorrowedSlot(REVNET_ID), bytes32(_ONE_TRILLION));
|
|
478
|
+
|
|
479
|
+
// Confirm the counter is at the overflow boundary.
|
|
480
|
+
assertEq(
|
|
481
|
+
LOANS_CONTRACT.totalLoansBorrowedFor(REVNET_ID),
|
|
482
|
+
_ONE_TRILLION,
|
|
483
|
+
"counter should be at _ONE_TRILLION after vm.store"
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
// Build an empty allowance (no permit2 needed for native token repayment).
|
|
487
|
+
JBSingleAllowance memory allowance;
|
|
488
|
+
|
|
489
|
+
// Expect the overflow revert from the partial-repay branch.
|
|
490
|
+
vm.expectRevert(REVLoans.REVLoans_LoanIdOverflow.selector);
|
|
491
|
+
|
|
492
|
+
// Attempt a partial repayment -- should revert because creating the replacement loan
|
|
493
|
+
// would exceed the _ONE_TRILLION loan ID namespace.
|
|
494
|
+
vm.prank(USER);
|
|
495
|
+
LOANS_CONTRACT.repayLoan{value: halfAmount}(loanId, halfAmount, 0, payable(USER), allowance);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.26;
|
|
2
|
+
pragma solidity ^0.8.26;
|
|
3
3
|
|
|
4
4
|
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
5
5
|
import "forge-std/Test.sol";
|
|
@@ -524,15 +524,11 @@ abstract contract ForkTestBase is TestBaseWorkflow {
|
|
|
524
524
|
hooks: IHooks(address(0))
|
|
525
525
|
});
|
|
526
526
|
|
|
527
|
-
// Pool is already initialized at
|
|
528
|
-
//
|
|
529
|
-
|
|
530
|
-
// At 1:1 price, full-range liquidity needs equal amounts of both tokens.
|
|
531
|
-
uint256 projectTokenAmount = liquidityTokenAmount;
|
|
532
|
-
|
|
533
|
-
// Fund LiquidityHelper with project tokens via JBTokens.mintFor (not deal).
|
|
527
|
+
// Pool is already initialized at fair issuance price by REVDeployer during deployment.
|
|
528
|
+
// At high tick (~69078 for 1000 tokens/ETH), full-range liquidity needs ~32x more project tokens than ETH.
|
|
529
|
+
// Mint 50x project tokens and use a smaller liquidity delta to stay within budget.
|
|
534
530
|
vm.prank(address(jbController()));
|
|
535
|
-
jbTokens().mintFor(address(liqHelper), revnetId,
|
|
531
|
+
jbTokens().mintFor(address(liqHelper), revnetId, liquidityTokenAmount * 50);
|
|
536
532
|
// Fund with ETH for the native currency side.
|
|
537
533
|
vm.deal(address(liqHelper), liquidityTokenAmount);
|
|
538
534
|
|
|
@@ -541,11 +537,12 @@ abstract contract ForkTestBase is TestBaseWorkflow {
|
|
|
541
537
|
vm.stopPrank();
|
|
542
538
|
|
|
543
539
|
// forge-lint: disable-next-line(unsafe-typecast)
|
|
544
|
-
int256 liquidityDelta = int256(liquidityTokenAmount /
|
|
540
|
+
int256 liquidityDelta = int256(liquidityTokenAmount / 50);
|
|
545
541
|
vm.prank(address(liqHelper));
|
|
546
542
|
liqHelper.addLiquidity{value: liquidityTokenAmount}(key, TICK_LOWER, TICK_UPPER, liquidityDelta);
|
|
547
543
|
|
|
548
|
-
|
|
544
|
+
// Mock geomean oracle at tick 69078 (~1000 tokens/ETH, matching INITIAL_ISSUANCE).
|
|
545
|
+
_mockOracle(liquidityDelta, 69_078, uint32(REV_DEPLOYER.DEFAULT_BUYBACK_TWAP_WINDOW()));
|
|
549
546
|
}
|
|
550
547
|
|
|
551
548
|
/// @notice Mock the IGeomeanOracle at address(0) for hookless pools.
|