@rev-net/core-v6 0.0.12 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUDIT_INSTRUCTIONS.md +295 -0
- package/CHANGE_LOG.md +316 -0
- package/README.md +2 -2
- package/RISKS.md +180 -35
- package/SKILLS.md +1 -1
- package/USER_JOURNEYS.md +489 -0
- package/package.json +9 -9
- package/script/Deploy.s.sol +40 -6
- package/script/helpers/RevnetCoreDeploymentLib.sol +7 -1
- package/src/REVDeployer.sol +63 -47
- package/src/REVLoans.sol +51 -15
- package/src/interfaces/IREVDeployer.sol +0 -1
- package/src/structs/REV721TiersHookFlags.sol +1 -0
- package/src/structs/REVAutoIssuance.sol +1 -0
- package/src/structs/REVBaseline721HookConfig.sol +1 -0
- package/src/structs/REVConfig.sol +1 -0
- package/src/structs/REVCroptopAllowedPost.sol +1 -0
- package/src/structs/REVDeploy721TiersHookConfig.sol +1 -0
- package/src/structs/REVDescription.sol +1 -0
- package/src/structs/REVLoan.sol +1 -0
- package/src/structs/REVLoanSource.sol +1 -0
- package/src/structs/REVStageConfig.sol +1 -0
- package/src/structs/REVSuckerDeploymentConfig.sol +1 -0
- package/test/REV.integrations.t.sol +132 -12
- package/test/REVAutoIssuanceFuzz.t.sol +23 -3
- package/test/REVDeployerRegressions.t.sol +35 -4
- package/test/REVInvincibility.t.sol +58 -8
- package/test/REVInvincibilityHandler.sol +29 -0
- package/test/REVLifecycle.t.sol +28 -3
- package/test/REVLoans.invariants.t.sol +52 -5
- package/test/REVLoansAttacks.t.sol +43 -5
- package/test/REVLoansFeeRecovery.t.sol +50 -11
- package/test/REVLoansFindings.t.sol +27 -3
- package/test/REVLoansRegressions.t.sol +25 -3
- package/test/REVLoansSourceFeeRecovery.t.sol +491 -0
- package/test/REVLoansSourced.t.sol +56 -7
- package/test/REVLoansUnSourced.t.sol +49 -5
- package/test/TestBurnHeldTokens.t.sol +32 -5
- package/test/TestCEIPattern.t.sol +26 -2
- package/test/TestCashOutCallerValidation.t.sol +30 -4
- package/test/TestConversionDocumentation.t.sol +26 -5
- package/test/TestCrossCurrencyReclaim.t.sol +584 -0
- package/test/TestCrossSourceReallocation.t.sol +26 -2
- package/test/TestERC2771MetaTx.t.sol +557 -0
- package/test/TestEmptyBuybackSpecs.t.sol +23 -3
- package/test/TestFlashLoanSurplus.t.sol +28 -3
- package/test/TestHookArrayOOB.t.sol +24 -4
- package/test/TestLiquidationBehavior.t.sol +26 -3
- package/test/TestLoanSourceRotation.t.sol +525 -0
- package/test/TestLongTailEconomics.t.sol +651 -0
- package/test/TestLowFindings.t.sol +65 -2
- package/test/TestMixedFixes.t.sol +28 -3
- package/test/TestPermit2Signatures.t.sol +657 -0
- package/test/TestReallocationSandwich.t.sol +384 -0
- package/test/TestRevnetRegressions.t.sol +324 -0
- package/test/TestSplitWeightAdjustment.t.sol +24 -2
- package/test/TestSplitWeightE2E.t.sol +29 -2
- package/test/TestSplitWeightFork.t.sol +46 -7
- package/test/TestStageTransitionBorrowable.t.sol +24 -2
- package/test/TestSwapTerminalPermission.t.sol +23 -3
- package/test/TestUint112Overflow.t.sol +28 -2
- package/test/TestZeroRepayment.t.sol +26 -2
- package/test/fork/ForkTestBase.sol +46 -3
- package/test/fork/TestCashOutFork.t.sol +1 -1
- package/test/fork/TestLoanBorrowFork.t.sol +1 -0
- package/test/fork/TestLoanCrossRulesetFork.t.sol +3 -1
- package/test/fork/TestLoanLiquidationFork.t.sol +1 -0
- package/test/fork/TestLoanReallocateFork.t.sol +1 -0
- package/test/fork/TestLoanRepayFork.t.sol +1 -0
- package/test/fork/TestLoanTransferFork.t.sol +133 -0
- package/test/fork/TestSplitWeightFork.t.sol +3 -0
- package/test/helpers/REVEmpty721Config.sol +1 -0
- package/test/mock/MockBuybackDataHook.sol +1 -0
- package/test/regression/TestBurnPermissionRequired.t.sol +267 -0
- package/test/regression/TestCrossRevnetLiquidation.t.sol +228 -0
- package/test/regression/TestCumulativeLoanCounter.t.sol +27 -4
- package/test/regression/TestLiquidateGapHandling.t.sol +29 -4
- package/test/regression/TestZeroPriceFeed.t.sol +396 -0
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity 0.8.26;
|
|
3
3
|
|
|
4
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
4
5
|
import "forge-std/Test.sol";
|
|
6
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
5
7
|
import /* {*} from */ "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
|
|
8
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
6
9
|
import /* {*} from */ "../../src/REVDeployer.sol";
|
|
10
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
7
11
|
import "@croptop/core-v6/src/CTPublisher.sol";
|
|
12
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
8
13
|
import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
|
|
14
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
9
15
|
import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
|
|
16
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
10
17
|
import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
|
|
18
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
11
19
|
import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
|
|
20
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
12
21
|
import "@bananapus/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
|
|
13
22
|
|
|
14
23
|
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
@@ -29,6 +38,7 @@ import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressReg
|
|
|
29
38
|
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
30
39
|
import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
|
|
31
40
|
import {IJBBuybackHookRegistry} from "@bananapus/buyback-hook-v6/src/interfaces/IJBBuybackHookRegistry.sol";
|
|
41
|
+
// forge-lint: disable-next-line(unused-import)
|
|
32
42
|
import {IJBPayHook} from "@bananapus/core-v6/src/interfaces/IJBPayHook.sol";
|
|
33
43
|
import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
|
|
34
44
|
import {JB721InitTiersConfig} from "@bananapus/721-hook-v6/src/structs/JB721InitTiersConfig.sol";
|
|
@@ -42,6 +52,7 @@ import {REVEmpty721Config} from "../helpers/REVEmpty721Config.sol";
|
|
|
42
52
|
// Buyback hook
|
|
43
53
|
import {JBBuybackHook} from "@bananapus/buyback-hook-v6/src/JBBuybackHook.sol";
|
|
44
54
|
import {JBBuybackHookRegistry} from "@bananapus/buyback-hook-v6/src/JBBuybackHookRegistry.sol";
|
|
55
|
+
// forge-lint: disable-next-line(unused-import)
|
|
45
56
|
import {IJBBuybackHook} from "@bananapus/buyback-hook-v6/src/interfaces/IJBBuybackHook.sol";
|
|
46
57
|
import {IGeomeanOracle} from "@bananapus/buyback-hook-v6/src/interfaces/IGeomeanOracle.sol";
|
|
47
58
|
|
|
@@ -50,10 +61,12 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
|
|
|
50
61
|
import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
|
|
51
62
|
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
|
|
52
63
|
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
|
|
64
|
+
// forge-lint: disable-next-line(unused-import)
|
|
53
65
|
import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
|
|
54
66
|
import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
|
|
55
67
|
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
|
|
56
68
|
import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
|
|
69
|
+
// forge-lint: disable-next-line(unused-import)
|
|
57
70
|
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
|
|
58
71
|
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
|
|
59
72
|
|
|
@@ -61,6 +74,7 @@ import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
|
|
|
61
74
|
|
|
62
75
|
/// @notice Helper that adds liquidity to a V4 pool via the unlock/callback pattern.
|
|
63
76
|
contract LiquidityHelper is IUnlockCallback {
|
|
77
|
+
// forge-lint: disable-next-line(screaming-snake-case-immutable)
|
|
64
78
|
IPoolManager public immutable poolManager;
|
|
65
79
|
|
|
66
80
|
enum Action {
|
|
@@ -96,7 +110,8 @@ contract LiquidityHelper is IUnlockCallback {
|
|
|
96
110
|
payable
|
|
97
111
|
{
|
|
98
112
|
bytes memory data =
|
|
99
|
-
|
|
113
|
+
// forge-lint: disable-next-line(named-struct-fields)
|
|
114
|
+
abi.encode(Action.ADD_LIQUIDITY, abi.encode(AddLiqParams(key, tickLower, tickUpper, liquidityDelta)));
|
|
100
115
|
poolManager.unlock(data);
|
|
101
116
|
}
|
|
102
117
|
|
|
@@ -110,7 +125,8 @@ contract LiquidityHelper is IUnlockCallback {
|
|
|
110
125
|
payable
|
|
111
126
|
{
|
|
112
127
|
bytes memory data =
|
|
113
|
-
|
|
128
|
+
// forge-lint: disable-next-line(named-struct-fields)
|
|
129
|
+
abi.encode(Action.SWAP, abi.encode(DoSwapParams(key, zeroForOne, amountSpecified, sqrtPriceLimitX96)));
|
|
114
130
|
poolManager.unlock(data);
|
|
115
131
|
}
|
|
116
132
|
|
|
@@ -152,7 +168,10 @@ contract LiquidityHelper is IUnlockCallback {
|
|
|
152
168
|
DoSwapParams memory params = abi.decode(data, (DoSwapParams));
|
|
153
169
|
|
|
154
170
|
BalanceDelta delta = poolManager.swap(
|
|
155
|
-
params.key,
|
|
171
|
+
params.key,
|
|
172
|
+
// forge-lint: disable-next-line(named-struct-fields)
|
|
173
|
+
SwapParams(params.zeroForOne, params.amountSpecified, params.sqrtPriceLimitX96),
|
|
174
|
+
""
|
|
156
175
|
);
|
|
157
176
|
|
|
158
177
|
if (delta.amount0() < 0) {
|
|
@@ -171,12 +190,14 @@ contract LiquidityHelper is IUnlockCallback {
|
|
|
171
190
|
|
|
172
191
|
function _settleIfNegative(Currency currency, int128 delta) internal {
|
|
173
192
|
if (delta >= 0) return;
|
|
193
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
174
194
|
uint256 amount = uint256(uint128(-delta));
|
|
175
195
|
|
|
176
196
|
if (currency.isAddressZero()) {
|
|
177
197
|
poolManager.settle{value: amount}();
|
|
178
198
|
} else {
|
|
179
199
|
poolManager.sync(currency);
|
|
200
|
+
// forge-lint: disable-next-line(erc20-unchecked-transfer)
|
|
180
201
|
IERC20(Currency.unwrap(currency)).transfer(address(poolManager), amount);
|
|
181
202
|
poolManager.settle();
|
|
182
203
|
}
|
|
@@ -184,6 +205,7 @@ contract LiquidityHelper is IUnlockCallback {
|
|
|
184
205
|
|
|
185
206
|
function _takeIfPositive(Currency currency, int128 delta) internal {
|
|
186
207
|
if (delta <= 0) return;
|
|
208
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
187
209
|
uint256 amount = uint256(uint128(delta));
|
|
188
210
|
poolManager.take(currency, address(this), amount);
|
|
189
211
|
}
|
|
@@ -212,24 +234,38 @@ abstract contract ForkTestBase is TestBaseWorkflow {
|
|
|
212
234
|
// ───────────────────────── State
|
|
213
235
|
// ─────────────────────────
|
|
214
236
|
|
|
237
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
215
238
|
REVDeployer REV_DEPLOYER;
|
|
239
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
216
240
|
JBBuybackHook BUYBACK_HOOK;
|
|
241
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
217
242
|
JBBuybackHookRegistry BUYBACK_REGISTRY;
|
|
243
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
218
244
|
JB721TiersHook EXAMPLE_HOOK;
|
|
245
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
219
246
|
IJB721TiersHookDeployer HOOK_DEPLOYER;
|
|
247
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
220
248
|
IJB721TiersHookStore HOOK_STORE;
|
|
249
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
221
250
|
IJBAddressRegistry ADDRESS_REGISTRY;
|
|
251
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
222
252
|
IREVLoans LOANS_CONTRACT;
|
|
253
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
223
254
|
IJBSuckerRegistry SUCKER_REGISTRY;
|
|
255
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
224
256
|
CTPublisher PUBLISHER;
|
|
225
257
|
IPoolManager poolManager;
|
|
226
258
|
LiquidityHelper liqHelper;
|
|
227
259
|
|
|
260
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
228
261
|
uint256 FEE_PROJECT_ID;
|
|
229
262
|
|
|
230
263
|
address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
|
|
264
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
231
265
|
address PAYER = makeAddr("payer");
|
|
266
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
232
267
|
address BORROWER = makeAddr("borrower");
|
|
268
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
233
269
|
address SPLIT_BENEFICIARY = makeAddr("splitBeneficiary");
|
|
234
270
|
|
|
235
271
|
// Tier configuration: 1 ETH tier with 30% split.
|
|
@@ -345,6 +381,7 @@ abstract contract ForkTestBase is TestBaseWorkflow {
|
|
|
345
381
|
});
|
|
346
382
|
|
|
347
383
|
cfg = REVConfig({
|
|
384
|
+
// forge-lint: disable-next-line(named-struct-fields)
|
|
348
385
|
description: REVDescription("Fork Test", "FORK", "ipfs://fork", "FORK_SALT"),
|
|
349
386
|
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
350
387
|
splitOperator: multisig(),
|
|
@@ -374,6 +411,7 @@ abstract contract ForkTestBase is TestBaseWorkflow {
|
|
|
374
411
|
votingUnits: 0,
|
|
375
412
|
reserveFrequency: 0,
|
|
376
413
|
reserveBeneficiary: address(0),
|
|
414
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
377
415
|
encodedIPFSUri: bytes32("tier1"),
|
|
378
416
|
category: 1,
|
|
379
417
|
discountPercent: 0,
|
|
@@ -405,6 +443,7 @@ abstract contract ForkTestBase is TestBaseWorkflow {
|
|
|
405
443
|
preventOverspending: false
|
|
406
444
|
})
|
|
407
445
|
}),
|
|
446
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
408
447
|
salt: bytes32("FORK_721"),
|
|
409
448
|
preventSplitOperatorAdjustingTiers: false,
|
|
410
449
|
preventSplitOperatorUpdatingMetadata: false,
|
|
@@ -420,6 +459,7 @@ abstract contract ForkTestBase is TestBaseWorkflow {
|
|
|
420
459
|
function _deployFeeProject(uint16 cashOutTaxRate) internal {
|
|
421
460
|
(REVConfig memory feeCfg, JBTerminalConfig[] memory feeTc, REVSuckerDeploymentConfig memory feeSdc) =
|
|
422
461
|
_buildMinimalConfig(cashOutTaxRate);
|
|
462
|
+
// forge-lint: disable-next-line(named-struct-fields)
|
|
423
463
|
feeCfg.description = REVDescription("Fee", "FEE", "ipfs://fee", "FEE_SALT");
|
|
424
464
|
|
|
425
465
|
vm.prank(multisig());
|
|
@@ -500,6 +540,7 @@ abstract contract ForkTestBase is TestBaseWorkflow {
|
|
|
500
540
|
IERC20(projectToken).approve(address(poolManager), type(uint256).max);
|
|
501
541
|
vm.stopPrank();
|
|
502
542
|
|
|
543
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
503
544
|
int256 liquidityDelta = int256(liquidityTokenAmount / 2);
|
|
504
545
|
vm.prank(address(liqHelper));
|
|
505
546
|
liqHelper.addLiquidity{value: liquidityTokenAmount}(key, TICK_LOWER, TICK_UPPER, liquidityDelta);
|
|
@@ -513,12 +554,14 @@ abstract contract ForkTestBase is TestBaseWorkflow {
|
|
|
513
554
|
|
|
514
555
|
int56[] memory tickCumulatives = new int56[](2);
|
|
515
556
|
tickCumulatives[0] = 0;
|
|
557
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
516
558
|
tickCumulatives[1] = int56(tick) * int56(int32(twapWindow));
|
|
517
559
|
|
|
518
560
|
uint136[] memory secondsPerLiquidityCumulativeX128s = new uint136[](2);
|
|
519
561
|
secondsPerLiquidityCumulativeX128s[0] = 0;
|
|
520
562
|
uint256 liq = uint256(liquidity > 0 ? liquidity : -liquidity);
|
|
521
563
|
if (liq == 0) liq = 1;
|
|
564
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
522
565
|
secondsPerLiquidityCumulativeX128s[1] = uint136((uint256(twapWindow) << 128) / liq);
|
|
523
566
|
|
|
524
567
|
vm.mockCall(
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity 0.8.26;
|
|
3
3
|
|
|
4
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
4
5
|
import "./ForkTestBase.sol";
|
|
5
|
-
import {JBCashOuts} from "@bananapus/core-v6/src/libraries/JBCashOuts.sol";
|
|
6
6
|
import {REVEmpty721Config} from "../helpers/REVEmpty721Config.sol";
|
|
7
7
|
|
|
8
8
|
/// @notice Fork tests for revnet cash-out scenarios with real Uniswap V4 buyback hook.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity 0.8.26;
|
|
3
3
|
|
|
4
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
4
5
|
import "./ForkTestBase.sol";
|
|
5
|
-
import {JBFees} from "@bananapus/core-v6/src/libraries/JBFees.sol";
|
|
6
6
|
import {REVEmpty721Config} from "../helpers/REVEmpty721Config.sol";
|
|
7
7
|
|
|
8
8
|
/// @notice Fork tests for loan lifecycle spanning multiple revnet stages (rulesets).
|
|
@@ -53,6 +53,7 @@ contract TestLoanCrossRulesetFork is ForkTestBase {
|
|
|
53
53
|
|
|
54
54
|
// Stage 2: low tax — starts after STAGE_DURATION.
|
|
55
55
|
stages[1] = REVStageConfig({
|
|
56
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
56
57
|
startsAtOrAfter: uint40(block.timestamp + STAGE_DURATION),
|
|
57
58
|
autoIssuances: new REVAutoIssuance[](0),
|
|
58
59
|
splitPercent: 0,
|
|
@@ -65,6 +66,7 @@ contract TestLoanCrossRulesetFork is ForkTestBase {
|
|
|
65
66
|
});
|
|
66
67
|
|
|
67
68
|
cfg = REVConfig({
|
|
69
|
+
// forge-lint: disable-next-line(named-struct-fields)
|
|
68
70
|
description: REVDescription("CrossStage", "XSTG", "ipfs://xstage", "XSTG_SALT"),
|
|
69
71
|
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
70
72
|
splitOperator: multisig(),
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
5
|
+
import "./ForkTestBase.sol";
|
|
6
|
+
|
|
7
|
+
/// @notice Fork tests for transferring loan NFTs and repaying from the new owner.
|
|
8
|
+
///
|
|
9
|
+
/// Covers: transfer + repay by new owner, original owner rejection after transfer, transfer + partial repay.
|
|
10
|
+
///
|
|
11
|
+
/// Run with: FOUNDRY_PROFILE=fork forge test --match-contract TestLoanTransferFork -vvv
|
|
12
|
+
contract TestLoanTransferFork is ForkTestBase {
|
|
13
|
+
uint256 revnetId;
|
|
14
|
+
uint256 loanId;
|
|
15
|
+
REVLoan loan;
|
|
16
|
+
uint256 borrowerTokens;
|
|
17
|
+
|
|
18
|
+
address newOwner = makeAddr("newOwner");
|
|
19
|
+
|
|
20
|
+
function setUp() public override {
|
|
21
|
+
super.setUp();
|
|
22
|
+
|
|
23
|
+
// Deploy fee project + revnet.
|
|
24
|
+
_deployFeeProject(5000);
|
|
25
|
+
revnetId = _deployRevnet(5000);
|
|
26
|
+
_setupPool(revnetId, 10_000 ether);
|
|
27
|
+
|
|
28
|
+
// Pay to create surplus.
|
|
29
|
+
_payRevnet(revnetId, PAYER, 10 ether);
|
|
30
|
+
_payRevnet(revnetId, BORROWER, 5 ether);
|
|
31
|
+
|
|
32
|
+
borrowerTokens = jbTokens().totalBalanceOf(BORROWER, revnetId);
|
|
33
|
+
|
|
34
|
+
// Create a loan with min prepaid fee.
|
|
35
|
+
(loanId, loan) = _createLoan(revnetId, BORROWER, borrowerTokens, LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// @notice Transfer loan NFT to a new owner, who then fully repays the loan.
|
|
39
|
+
function test_fork_transferLoan_newOwnerCanRepay() public {
|
|
40
|
+
// Transfer the loan NFT from BORROWER to newOwner.
|
|
41
|
+
vm.prank(BORROWER);
|
|
42
|
+
REVLoans(payable(address(LOANS_CONTRACT))).safeTransferFrom(BORROWER, newOwner, loanId);
|
|
43
|
+
|
|
44
|
+
// Verify newOwner is the loan NFT owner.
|
|
45
|
+
assertEq(_loanOwnerOf(loanId), newOwner, "newOwner should own the loan NFT after transfer");
|
|
46
|
+
|
|
47
|
+
// Fund newOwner with ETH for repayment.
|
|
48
|
+
vm.deal(newOwner, 100 ether);
|
|
49
|
+
|
|
50
|
+
JBSingleAllowance memory allowance;
|
|
51
|
+
|
|
52
|
+
// newOwner repays the loan in full.
|
|
53
|
+
vm.prank(newOwner);
|
|
54
|
+
LOANS_CONTRACT.repayLoan{value: loan.amount * 2}({
|
|
55
|
+
loanId: loanId,
|
|
56
|
+
maxRepayBorrowAmount: loan.amount * 2,
|
|
57
|
+
collateralCountToReturn: loan.collateral,
|
|
58
|
+
beneficiary: payable(newOwner),
|
|
59
|
+
allowance: allowance
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Loan NFT should be burned after full repay.
|
|
63
|
+
vm.expectRevert();
|
|
64
|
+
_loanOwnerOf(loanId);
|
|
65
|
+
|
|
66
|
+
// Collateral tokens should be minted to newOwner (the beneficiary).
|
|
67
|
+
uint256 newOwnerTokens = jbTokens().totalBalanceOf(newOwner, revnetId);
|
|
68
|
+
assertEq(newOwnerTokens, borrowerTokens, "collateral should be returned to newOwner");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// @notice After transferring the loan NFT, the original borrower cannot repay.
|
|
72
|
+
function test_fork_transferLoan_originalBorrowerCannotRepay() public {
|
|
73
|
+
// Transfer the loan NFT from BORROWER to newOwner.
|
|
74
|
+
vm.prank(BORROWER);
|
|
75
|
+
REVLoans(payable(address(LOANS_CONTRACT))).safeTransferFrom(BORROWER, newOwner, loanId);
|
|
76
|
+
|
|
77
|
+
// Fund BORROWER with ETH for the attempted repayment.
|
|
78
|
+
vm.deal(BORROWER, 100 ether);
|
|
79
|
+
|
|
80
|
+
JBSingleAllowance memory allowance;
|
|
81
|
+
|
|
82
|
+
// Original borrower tries to repay — should revert with REVLoans_Unauthorized.
|
|
83
|
+
vm.prank(BORROWER);
|
|
84
|
+
vm.expectRevert(abi.encodeWithSelector(REVLoans.REVLoans_Unauthorized.selector, BORROWER, newOwner));
|
|
85
|
+
LOANS_CONTRACT.repayLoan{value: loan.amount * 2}({
|
|
86
|
+
loanId: loanId,
|
|
87
|
+
maxRepayBorrowAmount: loan.amount * 2,
|
|
88
|
+
collateralCountToReturn: loan.collateral,
|
|
89
|
+
beneficiary: payable(BORROWER),
|
|
90
|
+
allowance: allowance
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// @notice Transfer loan NFT, new owner does a partial repay — old loan burned, new loan minted to new owner.
|
|
95
|
+
function test_fork_transferLoan_newOwnerPartialRepay() public {
|
|
96
|
+
// Transfer the loan NFT from BORROWER to newOwner.
|
|
97
|
+
vm.prank(BORROWER);
|
|
98
|
+
REVLoans(payable(address(LOANS_CONTRACT))).safeTransferFrom(BORROWER, newOwner, loanId);
|
|
99
|
+
|
|
100
|
+
// Fund newOwner with ETH for repayment.
|
|
101
|
+
vm.deal(newOwner, 100 ether);
|
|
102
|
+
|
|
103
|
+
uint256 halfCollateral = loan.collateral / 2;
|
|
104
|
+
|
|
105
|
+
JBSingleAllowance memory allowance;
|
|
106
|
+
|
|
107
|
+
// newOwner partially repays the loan (return half the collateral).
|
|
108
|
+
vm.prank(newOwner);
|
|
109
|
+
(uint256 newLoanId, REVLoan memory newLoan) = LOANS_CONTRACT.repayLoan{value: loan.amount * 2}({
|
|
110
|
+
loanId: loanId,
|
|
111
|
+
maxRepayBorrowAmount: loan.amount * 2,
|
|
112
|
+
collateralCountToReturn: halfCollateral,
|
|
113
|
+
beneficiary: payable(newOwner),
|
|
114
|
+
allowance: allowance
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Original loan NFT should be burned.
|
|
118
|
+
vm.expectRevert();
|
|
119
|
+
_loanOwnerOf(loanId);
|
|
120
|
+
|
|
121
|
+
// New loan should exist with reduced collateral.
|
|
122
|
+
assertGt(newLoanId, 0, "new loan should be created");
|
|
123
|
+
assertEq(newLoan.collateral, loan.collateral - halfCollateral, "new loan collateral should be reduced");
|
|
124
|
+
assertLt(newLoan.amount, loan.amount, "new loan borrow amount should be less");
|
|
125
|
+
|
|
126
|
+
// New loan NFT should be owned by newOwner.
|
|
127
|
+
assertEq(_loanOwnerOf(newLoanId), newOwner, "new loan NFT should be owned by newOwner");
|
|
128
|
+
|
|
129
|
+
// Half collateral should be returned to newOwner.
|
|
130
|
+
uint256 newOwnerTokens = jbTokens().totalBalanceOf(newOwner, revnetId);
|
|
131
|
+
assertEq(newOwnerTokens, halfCollateral, "half collateral should be returned to newOwner");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity 0.8.26;
|
|
3
3
|
|
|
4
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
4
5
|
import "./ForkTestBase.sol";
|
|
5
6
|
|
|
6
7
|
/// @notice Fork tests verifying that revnet 721 tier splits + real Uniswap V4 buyback hook produce correct token
|
|
@@ -50,6 +51,7 @@ contract TestSplitWeightFork is ForkTestBase {
|
|
|
50
51
|
vm.stopPrank();
|
|
51
52
|
|
|
52
53
|
// Add full-range liquidity at tick 0 (1:1 price).
|
|
54
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
53
55
|
int256 liquidityDelta = int256(ethLiq / 4);
|
|
54
56
|
vm.prank(address(liqHelper));
|
|
55
57
|
liqHelper.addLiquidity{value: ethLiq}(key, TICK_LOWER, TICK_UPPER, liquidityDelta);
|
|
@@ -66,6 +68,7 @@ contract TestSplitWeightFork is ForkTestBase {
|
|
|
66
68
|
uint160 sqrtPriceLimit = TickMath.getSqrtPriceAtTick(76_000);
|
|
67
69
|
|
|
68
70
|
vm.prank(address(liqHelper));
|
|
71
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
69
72
|
liqHelper.swap(key, zeroForOne, -int256(swapAmount), sqrtPriceLimit);
|
|
70
73
|
|
|
71
74
|
// Read the post-swap tick for the oracle mock.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
pragma solidity ^0.8.0;
|
|
3
3
|
|
|
4
4
|
import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
|
|
5
|
+
// forge-lint: disable-next-line(unused-import)
|
|
5
6
|
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
6
7
|
import {JB721InitTiersConfig} from "@bananapus/721-hook-v6/src/structs/JB721InitTiersConfig.sol";
|
|
7
8
|
import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
|
|
@@ -3,6 +3,7 @@ pragma solidity 0.8.26;
|
|
|
3
3
|
|
|
4
4
|
import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
|
|
5
5
|
import {IJBPayHook} from "@bananapus/core-v6/src/interfaces/IJBPayHook.sol";
|
|
6
|
+
// forge-lint: disable-next-line(unused-import)
|
|
6
7
|
import {IJBBuybackHook} from "@bananapus/buyback-hook-v6/src/interfaces/IJBBuybackHook.sol";
|
|
7
8
|
import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
|
|
8
9
|
import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";
|