@rev-net/core-v6 0.0.9 → 0.0.11
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/ARCHITECTURE.md +1 -1
- package/README.md +4 -4
- package/SKILLS.md +2 -8
- package/STYLE_GUIDE.md +127 -51
- package/docs/src/README.md +2 -2
- package/foundry.toml +3 -0
- package/package.json +12 -9
- package/remappings.txt +1 -1
- package/script/Deploy.s.sol +1 -1
- package/script/helpers/RevnetCoreDeploymentLib.sol +1 -1
- package/src/REVDeployer.sol +30 -26
- package/src/REVLoans.sol +1 -0
- package/test/{REVDeployerAuditRegressions.t.sol → REVDeployerRegressions.t.sol} +1 -1
- package/test/REVInvincibility.t.sol +15 -19
- package/test/REVLifecycle.t.sol +0 -1
- package/test/REVLoansAttacks.t.sol +3 -7
- package/test/REVLoansFeeRecovery.t.sol +0 -2
- package/test/{REVLoans_AuditFindings.t.sol → REVLoansFindings.t.sol} +6 -6
- package/test/{REVLoansAuditRegressions.t.sol → REVLoansRegressions.t.sol} +2 -2
- package/test/REVLoansSourced.t.sol +3 -1
- package/test/{TestPR26_BurnHeldTokens.t.sol → TestBurnHeldTokens.t.sol} +1 -1
- package/test/{TestPR27_CEIPattern.t.sol → TestCEIPattern.t.sol} +3 -3
- package/test/{TestPR15_CashOutCallerValidation.t.sol → TestCashOutCallerValidation.t.sol} +1 -3
- package/test/{TestPR09_ConversionDocumentation.t.sol → TestConversionDocumentation.t.sol} +1 -1
- package/test/{TestPR13_CrossSourceReallocation.t.sol → TestCrossSourceReallocation.t.sol} +1 -1
- package/test/{TestPR12_FlashLoanSurplus.t.sol → TestFlashLoanSurplus.t.sol} +1 -1
- package/test/{TestPR22_HookArrayOOB.t.sol → TestHookArrayOOB.t.sol} +1 -1
- package/test/{TestPR10_LiquidationBehavior.t.sol → TestLiquidationBehavior.t.sol} +4 -4
- package/test/{TestPR11_LowFindings.t.sol → TestLowFindings.t.sol} +1 -1
- package/test/{TestPR32_MixedFixes.t.sol → TestMixedFixes.t.sol} +1 -1
- package/test/TestSplitWeightFork.t.sol +118 -159
- package/test/{TestPR29_SwapTerminalPermission.t.sol → TestSwapTerminalPermission.t.sol} +1 -1
- package/test/{TestPR21_Uint112Overflow.t.sol → TestUint112Overflow.t.sol} +4 -4
- package/test/{TestPR16_ZeroRepayment.t.sol → TestZeroRepayment.t.sol} +4 -6
- package/test/fork/ForkTestBase.sol +83 -51
- package/test/fork/TestCashOutFork.t.sol +12 -11
- package/test/fork/TestLoanBorrowFork.t.sol +10 -12
- package/test/fork/TestLoanCrossRulesetFork.t.sol +300 -0
- package/test/fork/TestLoanLiquidationFork.t.sol +13 -8
- package/test/fork/TestLoanReallocateFork.t.sol +21 -12
- package/test/fork/TestLoanRepayFork.t.sol +17 -14
- package/test/fork/TestSplitWeightFork.t.sol +34 -34
- package/test/mock/MockBuybackDataHook.sol +4 -7
- package/test/mock/MockBuybackDataHookMintPath.sol +5 -8
- package/test/regression/{TestI20_CumulativeLoanCounter.t.sol → TestCumulativeLoanCounter.t.sol} +4 -4
- package/test/regression/{TestL27_LiquidateGapHandling.t.sol → TestLiquidateGapHandling.t.sol} +3 -3
|
@@ -42,7 +42,6 @@ import {REVCroptopAllowedPost} from "../../src/structs/REVCroptopAllowedPost.sol
|
|
|
42
42
|
import {JBBuybackHook} from "@bananapus/buyback-hook-v6/src/JBBuybackHook.sol";
|
|
43
43
|
import {JBBuybackHookRegistry} from "@bananapus/buyback-hook-v6/src/JBBuybackHookRegistry.sol";
|
|
44
44
|
import {IJBBuybackHook} from "@bananapus/buyback-hook-v6/src/interfaces/IJBBuybackHook.sol";
|
|
45
|
-
import {IWETH9} from "@bananapus/buyback-hook-v6/src/interfaces/external/IWETH9.sol";
|
|
46
45
|
import {IGeomeanOracle} from "@bananapus/buyback-hook-v6/src/interfaces/IGeomeanOracle.sol";
|
|
47
46
|
|
|
48
47
|
// Uniswap V4
|
|
@@ -53,7 +52,7 @@ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
|
|
|
53
52
|
import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
|
|
54
53
|
import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
|
|
55
54
|
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
|
|
56
|
-
import {ModifyLiquidityParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
|
|
55
|
+
import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
|
|
57
56
|
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
|
|
58
57
|
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
|
|
59
58
|
|
|
@@ -63,6 +62,11 @@ import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
|
|
|
63
62
|
contract LiquidityHelper is IUnlockCallback {
|
|
64
63
|
IPoolManager public immutable poolManager;
|
|
65
64
|
|
|
65
|
+
enum Action {
|
|
66
|
+
ADD_LIQUIDITY,
|
|
67
|
+
SWAP
|
|
68
|
+
}
|
|
69
|
+
|
|
66
70
|
struct AddLiqParams {
|
|
67
71
|
PoolKey key;
|
|
68
72
|
int24 tickLower;
|
|
@@ -70,6 +74,13 @@ contract LiquidityHelper is IUnlockCallback {
|
|
|
70
74
|
int256 liquidityDelta;
|
|
71
75
|
}
|
|
72
76
|
|
|
77
|
+
struct DoSwapParams {
|
|
78
|
+
PoolKey key;
|
|
79
|
+
bool zeroForOne;
|
|
80
|
+
int256 amountSpecified;
|
|
81
|
+
uint160 sqrtPriceLimitX96;
|
|
82
|
+
}
|
|
83
|
+
|
|
73
84
|
constructor(IPoolManager _poolManager) {
|
|
74
85
|
poolManager = _poolManager;
|
|
75
86
|
}
|
|
@@ -83,13 +94,38 @@ contract LiquidityHelper is IUnlockCallback {
|
|
|
83
94
|
external
|
|
84
95
|
payable
|
|
85
96
|
{
|
|
86
|
-
bytes memory data =
|
|
97
|
+
bytes memory data =
|
|
98
|
+
abi.encode(Action.ADD_LIQUIDITY, abi.encode(AddLiqParams(key, tickLower, tickUpper, liquidityDelta)));
|
|
99
|
+
poolManager.unlock(data);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function swap(
|
|
103
|
+
PoolKey calldata key,
|
|
104
|
+
bool zeroForOne,
|
|
105
|
+
int256 amountSpecified,
|
|
106
|
+
uint160 sqrtPriceLimitX96
|
|
107
|
+
)
|
|
108
|
+
external
|
|
109
|
+
payable
|
|
110
|
+
{
|
|
111
|
+
bytes memory data =
|
|
112
|
+
abi.encode(Action.SWAP, abi.encode(DoSwapParams(key, zeroForOne, amountSpecified, sqrtPriceLimitX96)));
|
|
87
113
|
poolManager.unlock(data);
|
|
88
114
|
}
|
|
89
115
|
|
|
90
116
|
function unlockCallback(bytes calldata data) external override returns (bytes memory) {
|
|
91
117
|
require(msg.sender == address(poolManager), "only PM");
|
|
92
118
|
|
|
119
|
+
(Action action, bytes memory inner) = abi.decode(data, (Action, bytes));
|
|
120
|
+
|
|
121
|
+
if (action == Action.ADD_LIQUIDITY) {
|
|
122
|
+
return _handleAddLiquidity(inner);
|
|
123
|
+
} else {
|
|
124
|
+
return _handleSwap(inner);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function _handleAddLiquidity(bytes memory data) internal returns (bytes memory) {
|
|
93
129
|
AddLiqParams memory params = abi.decode(data, (AddLiqParams));
|
|
94
130
|
|
|
95
131
|
(BalanceDelta callerDelta,) = poolManager.modifyLiquidity(
|
|
@@ -111,6 +147,27 @@ contract LiquidityHelper is IUnlockCallback {
|
|
|
111
147
|
return abi.encode(callerDelta);
|
|
112
148
|
}
|
|
113
149
|
|
|
150
|
+
function _handleSwap(bytes memory data) internal returns (bytes memory) {
|
|
151
|
+
DoSwapParams memory params = abi.decode(data, (DoSwapParams));
|
|
152
|
+
|
|
153
|
+
BalanceDelta delta = poolManager.swap(
|
|
154
|
+
params.key, SwapParams(params.zeroForOne, params.amountSpecified, params.sqrtPriceLimitX96), ""
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (delta.amount0() < 0) {
|
|
158
|
+
_settleIfNegative(params.key.currency0, delta.amount0());
|
|
159
|
+
} else {
|
|
160
|
+
_takeIfPositive(params.key.currency0, delta.amount0());
|
|
161
|
+
}
|
|
162
|
+
if (delta.amount1() < 0) {
|
|
163
|
+
_settleIfNegative(params.key.currency1, delta.amount1());
|
|
164
|
+
} else {
|
|
165
|
+
_takeIfPositive(params.key.currency1, delta.amount1());
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return abi.encode(delta);
|
|
169
|
+
}
|
|
170
|
+
|
|
114
171
|
function _settleIfNegative(Currency currency, int128 delta) internal {
|
|
115
172
|
if (delta >= 0) return;
|
|
116
173
|
uint256 amount = uint256(uint128(-delta));
|
|
@@ -146,11 +203,10 @@ abstract contract ForkTestBase is TestBaseWorkflow {
|
|
|
146
203
|
// ─────────────────────────
|
|
147
204
|
|
|
148
205
|
address constant POOL_MANAGER_ADDR = 0x000000000004444c5dc75cB358380D2e3dE08A90;
|
|
149
|
-
address constant WETH_ADDR = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
|
|
150
206
|
|
|
151
207
|
/// @notice Full-range tick bounds for tickSpacing = 60.
|
|
152
|
-
int24 constant TICK_LOWER = -
|
|
153
|
-
int24 constant TICK_UPPER =
|
|
208
|
+
int24 constant TICK_LOWER = -887_200;
|
|
209
|
+
int24 constant TICK_UPPER = 887_200;
|
|
154
210
|
|
|
155
211
|
// ───────────────────────── State
|
|
156
212
|
// ─────────────────────────
|
|
@@ -166,7 +222,6 @@ abstract contract ForkTestBase is TestBaseWorkflow {
|
|
|
166
222
|
IJBSuckerRegistry SUCKER_REGISTRY;
|
|
167
223
|
CTPublisher PUBLISHER;
|
|
168
224
|
IPoolManager poolManager;
|
|
169
|
-
IWETH9 weth;
|
|
170
225
|
LiquidityHelper liqHelper;
|
|
171
226
|
|
|
172
227
|
uint256 FEE_PROJECT_ID;
|
|
@@ -185,13 +240,8 @@ abstract contract ForkTestBase is TestBaseWorkflow {
|
|
|
185
240
|
// ─────────────────────────
|
|
186
241
|
|
|
187
242
|
function setUp() public virtual override {
|
|
188
|
-
// Fork mainnet
|
|
189
|
-
|
|
190
|
-
if (bytes(rpcUrl).length == 0) {
|
|
191
|
-
vm.skip(true);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
vm.createSelectFork(rpcUrl);
|
|
243
|
+
// Fork mainnet at a stable block — deterministic and post-V4 deployment.
|
|
244
|
+
vm.createSelectFork("ethereum", 21_700_000);
|
|
195
245
|
|
|
196
246
|
// Verify V4 PoolManager is deployed.
|
|
197
247
|
require(POOL_MANAGER_ADDR.code.length > 0, "PoolManager not deployed at expected address");
|
|
@@ -200,7 +250,6 @@ abstract contract ForkTestBase is TestBaseWorkflow {
|
|
|
200
250
|
super.setUp();
|
|
201
251
|
|
|
202
252
|
poolManager = IPoolManager(POOL_MANAGER_ADDR);
|
|
203
|
-
weth = IWETH9(WETH_ADDR);
|
|
204
253
|
liqHelper = new LiquidityHelper(poolManager);
|
|
205
254
|
|
|
206
255
|
FEE_PROJECT_ID = jbProjects().createFor(multisig());
|
|
@@ -220,8 +269,8 @@ abstract contract ForkTestBase is TestBaseWorkflow {
|
|
|
220
269
|
jbPrices(),
|
|
221
270
|
jbProjects(),
|
|
222
271
|
jbTokens(),
|
|
223
|
-
weth,
|
|
224
272
|
poolManager,
|
|
273
|
+
IHooks(address(0)), // oracleHook
|
|
225
274
|
address(0) // trustedForwarder
|
|
226
275
|
);
|
|
227
276
|
|
|
@@ -262,12 +311,6 @@ abstract contract ForkTestBase is TestBaseWorkflow {
|
|
|
262
311
|
vm.deal(BORROWER, 100 ether);
|
|
263
312
|
}
|
|
264
313
|
|
|
265
|
-
modifier onlyFork() {
|
|
266
|
-
string memory rpcUrl = vm.envOr("RPC_ETHEREUM_MAINNET", string(""));
|
|
267
|
-
if (bytes(rpcUrl).length == 0) return;
|
|
268
|
-
_;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
314
|
// ───────────────────────── Config Helpers
|
|
272
315
|
// ─────────────────────────
|
|
273
316
|
|
|
@@ -403,6 +446,8 @@ abstract contract ForkTestBase is TestBaseWorkflow {
|
|
|
403
446
|
function _deployRevnetWith721(uint16 cashOutTaxRate) internal returns (uint256 revnetId, IJB721TiersHook hook) {
|
|
404
447
|
(REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc) =
|
|
405
448
|
_buildMinimalConfig(cashOutTaxRate);
|
|
449
|
+
// Use a different salt to avoid CREATE2 collision with _deployRevnet's ERC-20.
|
|
450
|
+
cfg.description.salt = "FORK_721_SALT";
|
|
406
451
|
REVDeploy721TiersHookConfig memory hookConfig = _build721Config();
|
|
407
452
|
|
|
408
453
|
(revnetId, hook) = REV_DEPLOYER.deployWith721sFor({
|
|
@@ -418,55 +463,42 @@ abstract contract ForkTestBase is TestBaseWorkflow {
|
|
|
418
463
|
// ───────────────────────── Pool Helpers
|
|
419
464
|
// ─────────────────────────
|
|
420
465
|
|
|
421
|
-
/// @notice Set up a V4 pool for the revnet's project token /
|
|
466
|
+
/// @notice Set up a V4 pool for the revnet's project token / native ETH pair at 1:1 price.
|
|
422
467
|
function _setupPool(uint256 revnetId, uint256 liquidityTokenAmount) internal returns (PoolKey memory key) {
|
|
423
468
|
address projectToken = address(jbTokens().tokenOf(revnetId));
|
|
424
469
|
require(projectToken != address(0), "project token not deployed");
|
|
425
470
|
|
|
426
|
-
address
|
|
427
|
-
address
|
|
428
|
-
if (projectToken < WETH_ADDR) {
|
|
429
|
-
token0 = projectToken;
|
|
430
|
-
token1 = WETH_ADDR;
|
|
431
|
-
} else {
|
|
432
|
-
token0 = WETH_ADDR;
|
|
433
|
-
token1 = projectToken;
|
|
434
|
-
}
|
|
435
|
-
|
|
471
|
+
// Native ETH is represented as address(0) in V4 pool keys.
|
|
472
|
+
// address(0) is always less than any deployed token address.
|
|
436
473
|
key = PoolKey({
|
|
437
|
-
currency0: Currency.wrap(
|
|
438
|
-
currency1: Currency.wrap(
|
|
474
|
+
currency0: Currency.wrap(address(0)),
|
|
475
|
+
currency1: Currency.wrap(projectToken),
|
|
439
476
|
fee: REV_DEPLOYER.DEFAULT_BUYBACK_POOL_FEE(),
|
|
440
477
|
tickSpacing: REV_DEPLOYER.DEFAULT_BUYBACK_TICK_SPACING(),
|
|
441
478
|
hooks: IHooks(address(0))
|
|
442
479
|
});
|
|
443
480
|
|
|
444
|
-
|
|
445
|
-
|
|
481
|
+
// Pool is already initialized at 1:1 price by REVDeployer during deployment.
|
|
482
|
+
// Just add liquidity and mock the oracle.
|
|
483
|
+
|
|
484
|
+
// At 1:1 price, full-range liquidity needs equal amounts of both tokens.
|
|
485
|
+
uint256 projectTokenAmount = liquidityTokenAmount;
|
|
446
486
|
|
|
447
487
|
// Fund LiquidityHelper with project tokens via JBTokens.mintFor (not deal).
|
|
448
488
|
vm.prank(address(jbController()));
|
|
449
|
-
jbTokens().mintFor(address(liqHelper), revnetId,
|
|
489
|
+
jbTokens().mintFor(address(liqHelper), revnetId, projectTokenAmount);
|
|
490
|
+
// Fund with ETH for the native currency side.
|
|
450
491
|
vm.deal(address(liqHelper), liquidityTokenAmount);
|
|
451
|
-
vm.prank(address(liqHelper));
|
|
452
|
-
IWETH9(WETH_ADDR).deposit{value: liquidityTokenAmount}();
|
|
453
492
|
|
|
454
493
|
vm.startPrank(address(liqHelper));
|
|
455
494
|
IERC20(projectToken).approve(address(poolManager), type(uint256).max);
|
|
456
|
-
IERC20(WETH_ADDR).approve(address(poolManager), type(uint256).max);
|
|
457
495
|
vm.stopPrank();
|
|
458
496
|
|
|
459
497
|
int256 liquidityDelta = int256(liquidityTokenAmount / 2);
|
|
460
498
|
vm.prank(address(liqHelper));
|
|
461
|
-
liqHelper.addLiquidity(key, TICK_LOWER, TICK_UPPER, liquidityDelta);
|
|
499
|
+
liqHelper.addLiquidity{value: liquidityTokenAmount}(key, TICK_LOWER, TICK_UPPER, liquidityDelta);
|
|
462
500
|
|
|
463
501
|
_mockOracle(liquidityDelta, 0, uint32(REV_DEPLOYER.DEFAULT_BUYBACK_TWAP_WINDOW()));
|
|
464
|
-
|
|
465
|
-
uint256 twapWindow = REV_DEPLOYER.DEFAULT_BUYBACK_TWAP_WINDOW();
|
|
466
|
-
vm.prank(multisig());
|
|
467
|
-
BUYBACK_HOOK.setPoolFor({
|
|
468
|
-
projectId: revnetId, poolKey: key, twapWindow: twapWindow, terminalToken: JBConstants.NATIVE_TOKEN
|
|
469
|
-
});
|
|
470
502
|
}
|
|
471
503
|
|
|
472
504
|
/// @notice Mock the IGeomeanOracle at address(0) for hookless pools.
|
|
@@ -477,11 +509,11 @@ abstract contract ForkTestBase is TestBaseWorkflow {
|
|
|
477
509
|
tickCumulatives[0] = 0;
|
|
478
510
|
tickCumulatives[1] = int56(tick) * int56(int32(twapWindow));
|
|
479
511
|
|
|
480
|
-
|
|
512
|
+
uint136[] memory secondsPerLiquidityCumulativeX128s = new uint136[](2);
|
|
481
513
|
secondsPerLiquidityCumulativeX128s[0] = 0;
|
|
482
514
|
uint256 liq = uint256(liquidity > 0 ? liquidity : -liquidity);
|
|
483
515
|
if (liq == 0) liq = 1;
|
|
484
|
-
secondsPerLiquidityCumulativeX128s[1] =
|
|
516
|
+
secondsPerLiquidityCumulativeX128s[1] = uint136((uint256(twapWindow) << 128) / liq);
|
|
485
517
|
|
|
486
518
|
vm.mockCall(
|
|
487
519
|
address(0),
|
|
@@ -547,7 +579,7 @@ abstract contract ForkTestBase is TestBaseWorkflow {
|
|
|
547
579
|
}
|
|
548
580
|
|
|
549
581
|
/// @notice Build payment metadata with only 721 tier selection (no quote -> TWAP/spot fallback).
|
|
550
|
-
function _buildPayMetadataNoQuote(address hookMetadataTarget) internal
|
|
582
|
+
function _buildPayMetadataNoQuote(address hookMetadataTarget) internal pure returns (bytes memory) {
|
|
551
583
|
uint16[] memory tierIds = new uint16[](1);
|
|
552
584
|
tierIds[0] = 1;
|
|
553
585
|
bytes memory tierData = abi.encode(true, tierIds);
|
|
@@ -15,10 +15,6 @@ contract TestCashOutFork is ForkTestBase {
|
|
|
15
15
|
function setUp() public override {
|
|
16
16
|
super.setUp();
|
|
17
17
|
|
|
18
|
-
// Skip if no fork available.
|
|
19
|
-
string memory rpcUrl = vm.envOr("RPC_ETHEREUM_MAINNET", string(""));
|
|
20
|
-
if (bytes(rpcUrl).length == 0) return;
|
|
21
|
-
|
|
22
18
|
// Deploy fee project + revnet with 50% cashOutTaxRate.
|
|
23
19
|
_deployFeeProject(5000);
|
|
24
20
|
revnetId = _deployRevnet(5000);
|
|
@@ -34,7 +30,7 @@ contract TestCashOutFork is ForkTestBase {
|
|
|
34
30
|
}
|
|
35
31
|
|
|
36
32
|
/// @notice Cash out tokens and verify fee deduction, token burn, and bonding curve reclaim.
|
|
37
|
-
function test_fork_cashOut_normalWithFee() public
|
|
33
|
+
function test_fork_cashOut_normalWithFee() public {
|
|
38
34
|
uint256 payerTokens = jbTokens().totalBalanceOf(PAYER, revnetId);
|
|
39
35
|
uint256 cashOutCount = payerTokens / 2; // Cash out half.
|
|
40
36
|
|
|
@@ -78,7 +74,7 @@ contract TestCashOutFork is ForkTestBase {
|
|
|
78
74
|
}
|
|
79
75
|
|
|
80
76
|
/// @notice High tax rate (90%) produces small reclaim relative to pro-rata.
|
|
81
|
-
function test_fork_cashOut_highTaxRate() public
|
|
77
|
+
function test_fork_cashOut_highTaxRate() public {
|
|
82
78
|
// Deploy a separate revnet with 90% tax rate.
|
|
83
79
|
uint256 highTaxRevnet = _deployRevnet(9000);
|
|
84
80
|
_setupPool(highTaxRevnet, 10_000 ether);
|
|
@@ -111,7 +107,7 @@ contract TestCashOutFork is ForkTestBase {
|
|
|
111
107
|
}
|
|
112
108
|
|
|
113
109
|
/// @notice Sucker addresses get full pro-rata reclaim with 0% tax and no fee.
|
|
114
|
-
function test_fork_cashOut_suckerExempt() public
|
|
110
|
+
function test_fork_cashOut_suckerExempt() public {
|
|
115
111
|
address sucker = makeAddr("sucker");
|
|
116
112
|
vm.deal(sucker, 100 ether);
|
|
117
113
|
|
|
@@ -153,7 +149,7 @@ contract TestCashOutFork is ForkTestBase {
|
|
|
153
149
|
}
|
|
154
150
|
|
|
155
151
|
/// @notice After a payment with 30% tier split, surplus accounting reflects actual terminal balance.
|
|
156
|
-
function test_fork_cashOut_afterTierSplitPayment() public
|
|
152
|
+
function test_fork_cashOut_afterTierSplitPayment() public {
|
|
157
153
|
// Deploy revnet with 721 hook.
|
|
158
154
|
(uint256 splitRevnetId, IJB721TiersHook hook) = _deployRevnetWith721(5000);
|
|
159
155
|
_setupPool(splitRevnetId, 10_000 ether);
|
|
@@ -202,9 +198,14 @@ contract TestCashOutFork is ForkTestBase {
|
|
|
202
198
|
}
|
|
203
199
|
|
|
204
200
|
/// @notice Cash out before delay expires should revert.
|
|
205
|
-
function test_fork_cashOut_delayEnforcement() public
|
|
206
|
-
// Deploy a
|
|
207
|
-
|
|
201
|
+
function test_fork_cashOut_delayEnforcement() public {
|
|
202
|
+
// Deploy a revnet whose first stage started in the past → triggers cash-out delay.
|
|
203
|
+
(REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc) =
|
|
204
|
+
_buildMinimalConfig(5000);
|
|
205
|
+
cfg.stageConfigurations[0].startsAtOrAfter = uint40(block.timestamp - 1);
|
|
206
|
+
uint256 delayRevnet = REV_DEPLOYER.deployFor({
|
|
207
|
+
revnetId: 0, configuration: cfg, terminalConfigurations: tc, suckerDeploymentConfiguration: sdc
|
|
208
|
+
});
|
|
208
209
|
_setupPool(delayRevnet, 10_000 ether);
|
|
209
210
|
_payRevnet(delayRevnet, PAYER, 1 ether);
|
|
210
211
|
|
|
@@ -15,9 +15,6 @@ contract TestLoanBorrowFork is ForkTestBase {
|
|
|
15
15
|
function setUp() public override {
|
|
16
16
|
super.setUp();
|
|
17
17
|
|
|
18
|
-
string memory rpcUrl = vm.envOr("RPC_ETHEREUM_MAINNET", string(""));
|
|
19
|
-
if (bytes(rpcUrl).length == 0) return;
|
|
20
|
-
|
|
21
18
|
// Deploy fee project + revnet with 50% cashOutTaxRate.
|
|
22
19
|
_deployFeeProject(5000);
|
|
23
20
|
revnetId = _deployRevnet(5000);
|
|
@@ -31,7 +28,7 @@ contract TestLoanBorrowFork is ForkTestBase {
|
|
|
31
28
|
}
|
|
32
29
|
|
|
33
30
|
/// @notice Basic borrow: collateralize all borrower tokens, verify loan state.
|
|
34
|
-
function test_fork_borrow_basic() public
|
|
31
|
+
function test_fork_borrow_basic() public {
|
|
35
32
|
uint256 borrowerTokens = jbTokens().totalBalanceOf(BORROWER, revnetId);
|
|
36
33
|
|
|
37
34
|
uint256 borrowable = LOANS_CONTRACT.borrowableAmountFrom(
|
|
@@ -53,8 +50,11 @@ contract TestLoanBorrowFork is ForkTestBase {
|
|
|
53
50
|
assertEq(loan.collateral, borrowerTokens, "loan collateral should match");
|
|
54
51
|
assertEq(loan.createdAt, block.timestamp, "loan createdAt should be now");
|
|
55
52
|
|
|
56
|
-
// Borrower tokens
|
|
57
|
-
|
|
53
|
+
// Borrower's original tokens are burned as collateral, but the source fee payment back to the revnet mints
|
|
54
|
+
// some tokens to the borrower.
|
|
55
|
+
uint256 feeTokens = jbTokens().totalBalanceOf(BORROWER, revnetId);
|
|
56
|
+
assertGt(feeTokens, 0, "borrower should have tokens from source fee payment");
|
|
57
|
+
assertLt(feeTokens, borrowerTokens, "fee tokens should be less than original collateral");
|
|
58
58
|
|
|
59
59
|
// Borrower received ETH (net of fees).
|
|
60
60
|
assertGt(BORROWER.balance, borrowerEthBefore, "borrower should receive ETH");
|
|
@@ -76,7 +76,7 @@ contract TestLoanBorrowFork is ForkTestBase {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/// @notice Verify fee distribution: source fee (2.5%) + REV fee (1%) deducted correctly.
|
|
79
|
-
function test_fork_borrow_feeDistribution() public
|
|
79
|
+
function test_fork_borrow_feeDistribution() public {
|
|
80
80
|
uint256 borrowerTokens = jbTokens().totalBalanceOf(BORROWER, revnetId);
|
|
81
81
|
uint256 prepaidFeePercent = LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT(); // 25 = 2.5%
|
|
82
82
|
|
|
@@ -86,8 +86,6 @@ contract TestLoanBorrowFork is ForkTestBase {
|
|
|
86
86
|
|
|
87
87
|
// Record balances before.
|
|
88
88
|
uint256 borrowerEthBefore = BORROWER.balance;
|
|
89
|
-
uint256 revnetTerminalBefore = _terminalBalance(revnetId, JBConstants.NATIVE_TOKEN);
|
|
90
|
-
|
|
91
89
|
_grantBurnPermission(BORROWER, revnetId);
|
|
92
90
|
|
|
93
91
|
REVLoanSource memory source = _nativeLoanSource();
|
|
@@ -122,7 +120,7 @@ contract TestLoanBorrowFork is ForkTestBase {
|
|
|
122
120
|
}
|
|
123
121
|
|
|
124
122
|
/// @notice Borrow after a payment with 30% tier splits.
|
|
125
|
-
function test_fork_borrow_afterTierSplits() public
|
|
123
|
+
function test_fork_borrow_afterTierSplits() public {
|
|
126
124
|
// Deploy revnet with 721 hook.
|
|
127
125
|
(uint256 splitRevnetId, IJB721TiersHook hook) = _deployRevnetWith721(5000);
|
|
128
126
|
_setupPool(splitRevnetId, 10_000 ether);
|
|
@@ -142,8 +140,8 @@ contract TestLoanBorrowFork is ForkTestBase {
|
|
|
142
140
|
metadata: metadata
|
|
143
141
|
});
|
|
144
142
|
|
|
145
|
-
//
|
|
146
|
-
assertEq(borrowerTokens,
|
|
143
|
+
// Tier 1 costs 1 ETH with 30% split → 0.3 ETH to splits, 4.7 ETH minted at 1000 tokens/ETH = 4700 tokens.
|
|
144
|
+
assertEq(borrowerTokens, 4700e18, "should get 4700 tokens after tier split");
|
|
147
145
|
|
|
148
146
|
// Surplus should reflect actual terminal balance.
|
|
149
147
|
uint256 surplus = _terminalBalance(splitRevnetId, JBConstants.NATIVE_TOKEN);
|