@rev-net/core-v6 0.0.9 → 0.0.10
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
|
@@ -41,7 +41,6 @@ import {REVCroptopAllowedPost} from "../src/structs/REVCroptopAllowedPost.sol";
|
|
|
41
41
|
import {JBBuybackHook} from "@bananapus/buyback-hook-v6/src/JBBuybackHook.sol";
|
|
42
42
|
import {JBBuybackHookRegistry} from "@bananapus/buyback-hook-v6/src/JBBuybackHookRegistry.sol";
|
|
43
43
|
import {IJBBuybackHook} from "@bananapus/buyback-hook-v6/src/interfaces/IJBBuybackHook.sol";
|
|
44
|
-
import {IWETH9} from "@bananapus/buyback-hook-v6/src/interfaces/external/IWETH9.sol";
|
|
45
44
|
import {IGeomeanOracle} from "@bananapus/buyback-hook-v6/src/interfaces/IGeomeanOracle.sol";
|
|
46
45
|
|
|
47
46
|
// Uniswap V4
|
|
@@ -52,16 +51,21 @@ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
|
|
|
52
51
|
import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
|
|
53
52
|
import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
|
|
54
53
|
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
|
|
55
|
-
import {ModifyLiquidityParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
|
|
54
|
+
import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
|
|
56
55
|
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
|
|
57
56
|
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
|
|
58
57
|
|
|
59
58
|
import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
|
|
60
59
|
|
|
61
|
-
/// @notice Helper that adds liquidity to a V4 pool via the unlock/callback pattern.
|
|
60
|
+
/// @notice Helper that adds liquidity to and swaps on a V4 pool via the unlock/callback pattern.
|
|
62
61
|
contract LiquidityHelper is IUnlockCallback {
|
|
63
62
|
IPoolManager public immutable poolManager;
|
|
64
63
|
|
|
64
|
+
enum Action {
|
|
65
|
+
ADD_LIQUIDITY,
|
|
66
|
+
SWAP
|
|
67
|
+
}
|
|
68
|
+
|
|
65
69
|
struct AddLiqParams {
|
|
66
70
|
PoolKey key;
|
|
67
71
|
int24 tickLower;
|
|
@@ -69,6 +73,13 @@ contract LiquidityHelper is IUnlockCallback {
|
|
|
69
73
|
int256 liquidityDelta;
|
|
70
74
|
}
|
|
71
75
|
|
|
76
|
+
struct DoSwapParams {
|
|
77
|
+
PoolKey key;
|
|
78
|
+
bool zeroForOne;
|
|
79
|
+
int256 amountSpecified;
|
|
80
|
+
uint160 sqrtPriceLimitX96;
|
|
81
|
+
}
|
|
82
|
+
|
|
72
83
|
constructor(IPoolManager _poolManager) {
|
|
73
84
|
poolManager = _poolManager;
|
|
74
85
|
}
|
|
@@ -82,13 +93,38 @@ contract LiquidityHelper is IUnlockCallback {
|
|
|
82
93
|
external
|
|
83
94
|
payable
|
|
84
95
|
{
|
|
85
|
-
bytes memory data =
|
|
96
|
+
bytes memory data =
|
|
97
|
+
abi.encode(Action.ADD_LIQUIDITY, abi.encode(AddLiqParams(key, tickLower, tickUpper, liquidityDelta)));
|
|
98
|
+
poolManager.unlock(data);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function swap(
|
|
102
|
+
PoolKey calldata key,
|
|
103
|
+
bool zeroForOne,
|
|
104
|
+
int256 amountSpecified,
|
|
105
|
+
uint160 sqrtPriceLimitX96
|
|
106
|
+
)
|
|
107
|
+
external
|
|
108
|
+
payable
|
|
109
|
+
{
|
|
110
|
+
bytes memory data =
|
|
111
|
+
abi.encode(Action.SWAP, abi.encode(DoSwapParams(key, zeroForOne, amountSpecified, sqrtPriceLimitX96)));
|
|
86
112
|
poolManager.unlock(data);
|
|
87
113
|
}
|
|
88
114
|
|
|
89
115
|
function unlockCallback(bytes calldata data) external override returns (bytes memory) {
|
|
90
116
|
require(msg.sender == address(poolManager), "only PM");
|
|
91
117
|
|
|
118
|
+
(Action action, bytes memory inner) = abi.decode(data, (Action, bytes));
|
|
119
|
+
|
|
120
|
+
if (action == Action.ADD_LIQUIDITY) {
|
|
121
|
+
return _handleAddLiquidity(inner);
|
|
122
|
+
} else {
|
|
123
|
+
return _handleSwap(inner);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function _handleAddLiquidity(bytes memory data) internal returns (bytes memory) {
|
|
92
128
|
AddLiqParams memory params = abi.decode(data, (AddLiqParams));
|
|
93
129
|
|
|
94
130
|
(BalanceDelta callerDelta,) = poolManager.modifyLiquidity(
|
|
@@ -110,6 +146,28 @@ contract LiquidityHelper is IUnlockCallback {
|
|
|
110
146
|
return abi.encode(callerDelta);
|
|
111
147
|
}
|
|
112
148
|
|
|
149
|
+
function _handleSwap(bytes memory data) internal returns (bytes memory) {
|
|
150
|
+
DoSwapParams memory params = abi.decode(data, (DoSwapParams));
|
|
151
|
+
|
|
152
|
+
BalanceDelta delta = poolManager.swap(
|
|
153
|
+
params.key, SwapParams(params.zeroForOne, params.amountSpecified, params.sqrtPriceLimitX96), ""
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Settle (pay) what we owe, take what we're owed.
|
|
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
|
+
|
|
113
171
|
function _settleIfNegative(Currency currency, int128 delta) internal {
|
|
114
172
|
if (delta >= 0) return;
|
|
115
173
|
uint256 amount = uint256(uint128(-delta));
|
|
@@ -148,11 +206,10 @@ contract TestSplitWeightFork is TestBaseWorkflow {
|
|
|
148
206
|
// ─────────────────────────
|
|
149
207
|
|
|
150
208
|
address constant POOL_MANAGER_ADDR = 0x000000000004444c5dc75cB358380D2e3dE08A90;
|
|
151
|
-
address constant WETH_ADDR = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
|
|
152
209
|
|
|
153
|
-
/// @notice Full-range tick bounds for tickSpacing =
|
|
154
|
-
int24 constant TICK_LOWER = -
|
|
155
|
-
int24 constant TICK_UPPER =
|
|
210
|
+
/// @notice Full-range tick bounds for tickSpacing = 200.
|
|
211
|
+
int24 constant TICK_LOWER = -887_200;
|
|
212
|
+
int24 constant TICK_UPPER = 887_200;
|
|
156
213
|
|
|
157
214
|
// ───────────────────────── State
|
|
158
215
|
// ─────────────────────────
|
|
@@ -168,7 +225,6 @@ contract TestSplitWeightFork is TestBaseWorkflow {
|
|
|
168
225
|
IJBSuckerRegistry SUCKER_REGISTRY;
|
|
169
226
|
CTPublisher PUBLISHER;
|
|
170
227
|
IPoolManager poolManager;
|
|
171
|
-
IWETH9 weth;
|
|
172
228
|
LiquidityHelper liqHelper;
|
|
173
229
|
|
|
174
230
|
uint256 FEE_PROJECT_ID;
|
|
@@ -186,13 +242,8 @@ contract TestSplitWeightFork is TestBaseWorkflow {
|
|
|
186
242
|
// ─────────────────────────
|
|
187
243
|
|
|
188
244
|
function setUp() public override {
|
|
189
|
-
// Fork mainnet
|
|
190
|
-
|
|
191
|
-
if (bytes(rpcUrl).length == 0) {
|
|
192
|
-
vm.skip(true);
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
vm.createSelectFork(rpcUrl);
|
|
245
|
+
// Fork mainnet at a stable block — deterministic and post-V4 deployment.
|
|
246
|
+
vm.createSelectFork("ethereum", 21_700_000);
|
|
196
247
|
|
|
197
248
|
// Verify V4 PoolManager is deployed.
|
|
198
249
|
require(POOL_MANAGER_ADDR.code.length > 0, "PoolManager not deployed at expected address");
|
|
@@ -201,7 +252,6 @@ contract TestSplitWeightFork is TestBaseWorkflow {
|
|
|
201
252
|
super.setUp();
|
|
202
253
|
|
|
203
254
|
poolManager = IPoolManager(POOL_MANAGER_ADDR);
|
|
204
|
-
weth = IWETH9(WETH_ADDR);
|
|
205
255
|
liqHelper = new LiquidityHelper(poolManager);
|
|
206
256
|
|
|
207
257
|
FEE_PROJECT_ID = jbProjects().createFor(multisig());
|
|
@@ -221,8 +271,8 @@ contract TestSplitWeightFork is TestBaseWorkflow {
|
|
|
221
271
|
jbPrices(),
|
|
222
272
|
jbProjects(),
|
|
223
273
|
jbTokens(),
|
|
224
|
-
weth,
|
|
225
274
|
poolManager,
|
|
275
|
+
IHooks(address(0)), // oracleHook
|
|
226
276
|
address(0) // trustedForwarder
|
|
227
277
|
);
|
|
228
278
|
|
|
@@ -262,12 +312,6 @@ contract TestSplitWeightFork is TestBaseWorkflow {
|
|
|
262
312
|
vm.deal(PAYER, 100 ether);
|
|
263
313
|
}
|
|
264
314
|
|
|
265
|
-
modifier onlyFork() {
|
|
266
|
-
string memory rpcUrl = vm.envOr("RPC_ETHEREUM_MAINNET", string(""));
|
|
267
|
-
if (bytes(rpcUrl).length == 0) return;
|
|
268
|
-
_;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
315
|
// ───────────────────────── Helpers
|
|
272
316
|
// ─────────────────────────
|
|
273
317
|
|
|
@@ -401,68 +445,44 @@ contract TestSplitWeightFork is TestBaseWorkflow {
|
|
|
401
445
|
});
|
|
402
446
|
}
|
|
403
447
|
|
|
404
|
-
/// @notice Set up a V4 pool for the revnet's project token /
|
|
448
|
+
/// @notice Set up a V4 pool for the revnet's project token / native ETH pair and register it with the buyback hook.
|
|
405
449
|
function _setupPool(uint256 revnetId, uint256 liquidityTokenAmount) internal returns (PoolKey memory key) {
|
|
406
450
|
// Get the project token.
|
|
407
451
|
address projectToken = address(jbTokens().tokenOf(revnetId));
|
|
408
452
|
require(projectToken != address(0), "project token not deployed");
|
|
409
453
|
|
|
410
|
-
//
|
|
411
|
-
address
|
|
412
|
-
address token1;
|
|
413
|
-
if (projectToken < WETH_ADDR) {
|
|
414
|
-
token0 = projectToken;
|
|
415
|
-
token1 = WETH_ADDR;
|
|
416
|
-
} else {
|
|
417
|
-
token0 = WETH_ADDR;
|
|
418
|
-
token1 = projectToken;
|
|
419
|
-
}
|
|
420
|
-
|
|
454
|
+
// Native ETH is represented as address(0) in V4 pool keys.
|
|
455
|
+
// address(0) is always less than any deployed token address.
|
|
421
456
|
key = PoolKey({
|
|
422
|
-
currency0: Currency.wrap(
|
|
423
|
-
currency1: Currency.wrap(
|
|
457
|
+
currency0: Currency.wrap(address(0)),
|
|
458
|
+
currency1: Currency.wrap(projectToken),
|
|
424
459
|
fee: REV_DEPLOYER.DEFAULT_BUYBACK_POOL_FEE(),
|
|
425
460
|
tickSpacing: REV_DEPLOYER.DEFAULT_BUYBACK_TICK_SPACING(),
|
|
426
461
|
hooks: IHooks(address(0))
|
|
427
462
|
});
|
|
428
463
|
|
|
429
|
-
//
|
|
430
|
-
|
|
431
|
-
poolManager.initialize(key, sqrtPrice);
|
|
464
|
+
// Pool is already initialized at 1:1 price by REVDeployer during deployment.
|
|
465
|
+
// Just add liquidity and mock the oracle.
|
|
432
466
|
|
|
433
467
|
// Fund LiquidityHelper with project tokens via JBTokens.mintFor (not deal).
|
|
434
468
|
// deal() skips ERC20Votes checkpoints, causing underflow when tokens are burned.
|
|
435
469
|
vm.prank(address(jbController()));
|
|
436
470
|
jbTokens().mintFor(address(liqHelper), revnetId, liquidityTokenAmount);
|
|
471
|
+
// Fund with ETH for the native currency side.
|
|
437
472
|
vm.deal(address(liqHelper), liquidityTokenAmount);
|
|
438
|
-
vm.prank(address(liqHelper));
|
|
439
|
-
IWETH9(WETH_ADDR).deposit{value: liquidityTokenAmount}();
|
|
440
473
|
|
|
441
|
-
// Approve PoolManager to spend tokens from LiquidityHelper.
|
|
474
|
+
// Approve PoolManager to spend project tokens from LiquidityHelper.
|
|
442
475
|
vm.startPrank(address(liqHelper));
|
|
443
476
|
IERC20(projectToken).approve(address(poolManager), type(uint256).max);
|
|
444
|
-
IERC20(WETH_ADDR).approve(address(poolManager), type(uint256).max);
|
|
445
477
|
vm.stopPrank();
|
|
446
478
|
|
|
447
479
|
// Add full-range liquidity.
|
|
448
480
|
int256 liquidityDelta = int256(liquidityTokenAmount / 2);
|
|
449
481
|
vm.prank(address(liqHelper));
|
|
450
|
-
liqHelper.addLiquidity(key, TICK_LOWER, TICK_UPPER, liquidityDelta);
|
|
482
|
+
liqHelper.addLiquidity{value: liquidityTokenAmount}(key, TICK_LOWER, TICK_UPPER, liquidityDelta);
|
|
451
483
|
|
|
452
484
|
// Mock the oracle at address(0) for hookless pools.
|
|
453
|
-
// The buyback hook calls IGeomeanOracle(address(key.hooks)).observe() for TWAP.
|
|
454
|
-
// Since hooks = address(0), we need code there + a mock response.
|
|
455
|
-
// tick=0 means 1:1 price → TWAP says pool rate is ~1 token/WETH → minting wins.
|
|
456
485
|
_mockOracle(liquidityDelta, 0, uint32(REV_DEPLOYER.DEFAULT_BUYBACK_TWAP_WINDOW()));
|
|
457
|
-
|
|
458
|
-
// Cache immutables before prank (vm.prank only applies to the next call).
|
|
459
|
-
uint256 twapWindow = REV_DEPLOYER.DEFAULT_BUYBACK_TWAP_WINDOW();
|
|
460
|
-
|
|
461
|
-
// Register pool with buyback hook via split operator (multisig has SET_BUYBACK_POOL permission).
|
|
462
|
-
vm.prank(multisig());
|
|
463
|
-
BUYBACK_HOOK.setPoolFor({
|
|
464
|
-
projectId: revnetId, poolKey: key, twapWindow: twapWindow, terminalToken: JBConstants.NATIVE_TOKEN
|
|
465
|
-
});
|
|
466
486
|
}
|
|
467
487
|
|
|
468
488
|
/// @notice Mock the IGeomeanOracle at address(0) for hookless pools.
|
|
@@ -478,11 +498,11 @@ contract TestSplitWeightFork is TestBaseWorkflow {
|
|
|
478
498
|
// arithmeticMeanTick = (tickCumulatives[1] - tickCumulatives[0]) / twapWindow = tick
|
|
479
499
|
tickCumulatives[1] = int56(tick) * int56(int32(twapWindow));
|
|
480
500
|
|
|
481
|
-
|
|
501
|
+
uint136[] memory secondsPerLiquidityCumulativeX128s = new uint136[](2);
|
|
482
502
|
secondsPerLiquidityCumulativeX128s[0] = 0;
|
|
483
503
|
uint256 liq = uint256(liquidity > 0 ? liquidity : -liquidity);
|
|
484
504
|
if (liq == 0) liq = 1;
|
|
485
|
-
secondsPerLiquidityCumulativeX128s[1] =
|
|
505
|
+
secondsPerLiquidityCumulativeX128s[1] = uint136((uint256(twapWindow) << 128) / liq);
|
|
486
506
|
|
|
487
507
|
vm.mockCall(
|
|
488
508
|
address(0),
|
|
@@ -523,7 +543,7 @@ contract TestSplitWeightFork is TestBaseWorkflow {
|
|
|
523
543
|
}
|
|
524
544
|
|
|
525
545
|
/// @notice Build payment metadata with only 721 tier selection (no quote → TWAP/spot fallback).
|
|
526
|
-
function _buildPayMetadataNoQuote(address hookMetadataTarget) internal
|
|
546
|
+
function _buildPayMetadataNoQuote(address hookMetadataTarget) internal pure returns (bytes memory) {
|
|
527
547
|
uint16[] memory tierIds = new uint16[](1);
|
|
528
548
|
tierIds[0] = 1;
|
|
529
549
|
bytes memory tierData = abi.encode(true, tierIds);
|
|
@@ -543,122 +563,64 @@ contract TestSplitWeightFork is TestBaseWorkflow {
|
|
|
543
563
|
/// @notice SWAP PATH: Pool offers good rate → buyback hook swaps on AMM instead of minting.
|
|
544
564
|
/// With 30% tier split, the buyback should swap with 0.7 ETH worth.
|
|
545
565
|
/// Terminal mints 0 tokens (weight=0), buyback hook mints via controller after swap.
|
|
546
|
-
function test_fork_swapPath_splitWithBuyback() public
|
|
566
|
+
function test_fork_swapPath_splitWithBuyback() public {
|
|
547
567
|
(uint256 revnetId, IJB721TiersHook hook) = _deployRevnetWith721();
|
|
548
568
|
|
|
549
|
-
//
|
|
550
|
-
//
|
|
551
|
-
// Wait — at 1:1 pool price, 1 WETH gets ~1 token. Minting gets 1000 tokens.
|
|
552
|
-
// So minting is better → buyback will NOT swap.
|
|
553
|
-
// To make swap win, we need the pool to offer MORE tokens per WETH than minting.
|
|
554
|
-
// Minting rate = 1000 tokens/ETH (before split reduction, the buyback sees reduced weight).
|
|
555
|
-
//
|
|
556
|
-
// After REVDeployer scales weight: weight = 1000e18 * 0.7e18 / 1e18 = 700e18
|
|
557
|
-
// The buyback hook receives weight=700e18 and amount=0.7 ETH.
|
|
558
|
-
// tokenCountWithoutHook = mulDiv(0.7e18, 700e18, 1e18) = 490 tokens.
|
|
559
|
-
//
|
|
560
|
-
// Wait, that's wrong. Let me re-trace:
|
|
561
|
-
// REVDeployer.beforePayRecordedWith:
|
|
562
|
-
// 1. 721 hook returns splitAmount=0.3 ETH → projectAmount = 0.7 ETH
|
|
563
|
-
// 2. buybackHookContext.amount.value = 0.7 ETH, weight = context.weight = 1000e18
|
|
564
|
-
// 3. Buyback hook sees: amountToSwapWith = 0.7 ETH, weight = 1000e18
|
|
565
|
-
// tokenCountWithoutHook = mulDiv(0.7e18, 1000e18, 1e18) = 700 tokens
|
|
566
|
-
// 4. If pool offers > 700 tokens for 0.7 WETH → swap wins
|
|
567
|
-
// 5. If pool offers < 700 tokens → mint wins
|
|
568
|
-
//
|
|
569
|
-
// At 1:1 pool price, 0.7 WETH gets ~0.7 tokens (after fees). That's way less than 700.
|
|
570
|
-
// We need a pool priced so projectToken is CHEAP — e.g., 1 WETH = 2000 tokens.
|
|
571
|
-
//
|
|
572
|
-
// Let's create a pool at a tick where projectToken is very cheap.
|
|
573
|
-
// tick = -69_000 gives approximately 1 WETH = 1000 tokens. We want more than 700 for 0.7 WETH.
|
|
574
|
-
// Actually, let's just seed the pool with lots of project tokens and little WETH.
|
|
575
|
-
// This naturally makes project tokens cheaper.
|
|
576
|
-
|
|
577
|
-
// Instead of tick manipulation, let's just use a pool at tick 0 (1:1) but seed asymmetrically:
|
|
578
|
-
// Lots of project tokens, little WETH → effective price favors the buyer.
|
|
579
|
-
// Actually V4 pool price is set at initialization (sqrtPriceX96), seeding doesn't change the tick.
|
|
580
|
-
//
|
|
581
|
-
// Let's initialize at a tick where 1 WETH = many project tokens.
|
|
582
|
-
// For swap to win: pool must give > 700 tokens for 0.7 WETH.
|
|
583
|
-
// Rate needed: > 1000 tokens/WETH.
|
|
584
|
-
// Use tick = -69082 which gives ~1:1000 ratio (1 WETH ≈ 1000 tokens).
|
|
585
|
-
// With 0.3% fee and slippage, it might give ~997, which is still > 700. Swap wins.
|
|
569
|
+
// We need to initialize the pool and get the price to favor buying project tokens: > 1000 tokens/ETH.
|
|
570
|
+
// Strategy: initialize pool, add liquidity, then swap project tokens for ETH to move the tick.
|
|
586
571
|
|
|
587
572
|
address projectToken = address(jbTokens().tokenOf(revnetId));
|
|
588
573
|
require(projectToken != address(0), "project token not deployed");
|
|
589
574
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
// Build sorted pool key.
|
|
593
|
-
address token0 = projectTokenIs0 ? projectToken : WETH_ADDR;
|
|
594
|
-
address token1 = projectTokenIs0 ? WETH_ADDR : projectToken;
|
|
595
|
-
|
|
575
|
+
// Native ETH is address(0), always less than any deployed token.
|
|
596
576
|
PoolKey memory key = PoolKey({
|
|
597
|
-
currency0: Currency.wrap(
|
|
598
|
-
currency1: Currency.wrap(
|
|
577
|
+
currency0: Currency.wrap(address(0)),
|
|
578
|
+
currency1: Currency.wrap(projectToken),
|
|
599
579
|
fee: REV_DEPLOYER.DEFAULT_BUYBACK_POOL_FEE(),
|
|
600
580
|
tickSpacing: REV_DEPLOYER.DEFAULT_BUYBACK_TICK_SPACING(),
|
|
601
581
|
hooks: IHooks(address(0))
|
|
602
582
|
});
|
|
603
583
|
|
|
604
|
-
//
|
|
605
|
-
// If projectToken is token0: price = token1/token0 = WETH/projectToken.
|
|
606
|
-
// We want projectToken cheap → WETH/projectToken high → tick positive.
|
|
607
|
-
// tick ~= 76_000 → price ~= 2000.
|
|
608
|
-
// If WETH is token0: price = token1/token0 = projectToken/WETH.
|
|
609
|
-
// We want projectToken/WETH high → tick positive.
|
|
610
|
-
// tick ~= 76_000 → price ~= 2000.
|
|
611
|
-
// Either way: positive tick ≈ 2000 of token1 per token0.
|
|
612
|
-
//
|
|
613
|
-
// But we want "1 WETH = 2000 projectTokens".
|
|
614
|
-
// If projectToken is token0: price = WETH per projectToken = 1/2000 → negative tick.
|
|
615
|
-
// tick ≈ -76_000.
|
|
616
|
-
// If WETH is token0: price = projectToken per WETH = 2000 → positive tick.
|
|
617
|
-
// tick ≈ 76_000.
|
|
618
|
-
int24 initTick;
|
|
619
|
-
if (projectTokenIs0) {
|
|
620
|
-
// price = WETH/projectToken = 0.0005 → tick ≈ -76_000
|
|
621
|
-
initTick = -76_020; // Rounded to tickSpacing=60
|
|
622
|
-
} else {
|
|
623
|
-
// price = projectToken/WETH = 2000 → tick ≈ 76_000
|
|
624
|
-
initTick = 76_020;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
uint160 sqrtPrice = TickMath.getSqrtPriceAtTick(initTick);
|
|
628
|
-
poolManager.initialize(key, sqrtPrice);
|
|
584
|
+
// Pool is already initialized at 1:1 price by REVDeployer during deployment.
|
|
629
585
|
|
|
630
586
|
// Seed liquidity. We need both tokens.
|
|
631
587
|
// IMPORTANT: Use JBTokens.mintFor (not deal) so ERC20Votes checkpoints are updated.
|
|
632
|
-
|
|
633
|
-
uint256
|
|
634
|
-
uint256 wethLiq = 5000e18; // some WETH
|
|
588
|
+
uint256 projectLiq = 10_000_000e18;
|
|
589
|
+
uint256 ethLiq = 5000e18;
|
|
635
590
|
|
|
636
591
|
vm.prank(address(jbController()));
|
|
637
592
|
jbTokens().mintFor(address(liqHelper), revnetId, projectLiq);
|
|
638
|
-
vm.deal(address(liqHelper),
|
|
639
|
-
vm.prank(address(liqHelper));
|
|
640
|
-
IWETH9(WETH_ADDR).deposit{value: wethLiq}();
|
|
593
|
+
vm.deal(address(liqHelper), ethLiq);
|
|
641
594
|
|
|
642
595
|
vm.startPrank(address(liqHelper));
|
|
643
596
|
IERC20(projectToken).approve(address(poolManager), type(uint256).max);
|
|
644
|
-
IERC20(WETH_ADDR).approve(address(poolManager), type(uint256).max);
|
|
645
597
|
vm.stopPrank();
|
|
646
598
|
|
|
647
|
-
// Add full-range liquidity.
|
|
648
|
-
int256 liquidityDelta = int256(
|
|
599
|
+
// Add full-range liquidity at tick 0 (1:1 price).
|
|
600
|
+
int256 liquidityDelta = int256(ethLiq / 4);
|
|
649
601
|
vm.prank(address(liqHelper));
|
|
650
|
-
liqHelper.addLiquidity(key, TICK_LOWER, TICK_UPPER, liquidityDelta);
|
|
602
|
+
liqHelper.addLiquidity{value: ethLiq}(key, TICK_LOWER, TICK_UPPER, liquidityDelta);
|
|
651
603
|
|
|
652
|
-
//
|
|
653
|
-
// This makes
|
|
654
|
-
|
|
604
|
+
// Swap a large amount of project tokens for ETH to move the price.
|
|
605
|
+
// This makes project tokens cheaper (more tokens per ETH) so the swap path wins.
|
|
606
|
+
uint256 swapAmount = 5_000_000e18;
|
|
607
|
+
vm.prank(address(jbController()));
|
|
608
|
+
jbTokens().mintFor(address(liqHelper), revnetId, swapAmount);
|
|
655
609
|
|
|
656
|
-
//
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
610
|
+
// currency0 is native ETH (address(0)), currency1 is projectToken.
|
|
611
|
+
// To sell projectToken for ETH (making project tokens cheaper), swap 1->0 (zeroForOne = false).
|
|
612
|
+
// zeroForOne=false pushes sqrtPrice up (more projectTokens per ETH).
|
|
613
|
+
bool zeroForOne = false;
|
|
614
|
+
uint160 sqrtPriceLimit = TickMath.getSqrtPriceAtTick(76_000);
|
|
615
|
+
|
|
616
|
+
vm.prank(address(liqHelper));
|
|
617
|
+
liqHelper.swap(key, zeroForOne, -int256(swapAmount), sqrtPriceLimit);
|
|
618
|
+
|
|
619
|
+
// Read the post-swap tick for the oracle mock.
|
|
620
|
+
(, int24 postSwapTick,,) = poolManager.getSlot0(key.toId());
|
|
621
|
+
|
|
622
|
+
// Mock the TWAP oracle to report the post-swap tick (so buyback hook sees the real price).
|
|
623
|
+
_mockOracle(liquidityDelta, postSwapTick, uint32(REV_DEPLOYER.DEFAULT_BUYBACK_TWAP_WINDOW()));
|
|
662
624
|
|
|
663
625
|
// Build metadata: mint tier 1 + quote for swap.
|
|
664
626
|
// The quote tells buyback to swap with the full amount, expecting at least 1 token out.
|
|
@@ -669,9 +631,6 @@ contract TestSplitWeightFork is TestBaseWorkflow {
|
|
|
669
631
|
minimumSwapAmountOut: 1 // Accept any amount from swap
|
|
670
632
|
});
|
|
671
633
|
|
|
672
|
-
// Record payer balance before.
|
|
673
|
-
uint256 payerBalBefore = jbTokens().totalBalanceOf(PAYER, revnetId);
|
|
674
|
-
|
|
675
634
|
// Pay 1 ETH through the terminal.
|
|
676
635
|
vm.prank(PAYER);
|
|
677
636
|
uint256 terminalTokensReturned = jbMultiTerminal().pay{value: 1 ether}({
|
|
@@ -701,11 +660,11 @@ contract TestSplitWeightFork is TestBaseWorkflow {
|
|
|
701
660
|
/// @notice MINT PATH: Pool offers bad rate → buyback decides minting is better.
|
|
702
661
|
/// With 30% tier split, REVDeployer scales weight from 1000e18 to 700e18.
|
|
703
662
|
/// Terminal mints 700 tokens.
|
|
704
|
-
function test_fork_mintPath_splitWithBuyback() public
|
|
663
|
+
function test_fork_mintPath_splitWithBuyback() public {
|
|
705
664
|
(uint256 revnetId, IJB721TiersHook hook) = _deployRevnetWith721();
|
|
706
665
|
|
|
707
666
|
// Set up pool with 1:1 price. At this price:
|
|
708
|
-
// 0.7
|
|
667
|
+
// 0.7 ETH → ~0.7 tokens from pool (after fees).
|
|
709
668
|
// Direct minting: 700 tokens.
|
|
710
669
|
// Minting wins by a huge margin → buyback returns context.weight unchanged.
|
|
711
670
|
_setupPool(revnetId, 10_000 ether);
|
|
@@ -713,7 +672,7 @@ contract TestSplitWeightFork is TestBaseWorkflow {
|
|
|
713
672
|
// Build metadata: mint tier 1 + quote for "swap" with 0.7 ETH, but expect many tokens (forces mint path).
|
|
714
673
|
// When minimumSwapAmountOut > actual pool output, the buyback hook falls back to minting.
|
|
715
674
|
// Actually the buyback hook uses max(payerQuote, twapQuote). If we set minimumSwapAmountOut=0,
|
|
716
|
-
// it'll use the TWAP/spot quote. At 1:1 pool price, spot says ~0.7 tokens for 0.7
|
|
675
|
+
// it'll use the TWAP/spot quote. At 1:1 pool price, spot says ~0.7 tokens for 0.7 ETH.
|
|
717
676
|
// tokenCountWithoutHook = 700 tokens. 700 > ~0.7 → mint wins.
|
|
718
677
|
// We don't even need quote metadata — the spot fallback handles it.
|
|
719
678
|
address metadataTarget = hook.METADATA_ID_TARGET();
|
|
@@ -742,7 +701,7 @@ contract TestSplitWeightFork is TestBaseWorkflow {
|
|
|
742
701
|
}
|
|
743
702
|
|
|
744
703
|
/// @notice MINT PATH without splits: baseline confirming 1000 tokens for 1 ETH.
|
|
745
|
-
function test_fork_mintPath_noSplits_fullTokens() public
|
|
704
|
+
function test_fork_mintPath_noSplits_fullTokens() public {
|
|
746
705
|
(uint256 revnetId,) = _deployRevnetWith721();
|
|
747
706
|
_setupPool(revnetId, 10_000 ether);
|
|
748
707
|
|
|
@@ -764,7 +723,7 @@ contract TestSplitWeightFork is TestBaseWorkflow {
|
|
|
764
723
|
}
|
|
765
724
|
|
|
766
725
|
/// @notice Invariant: tokens / projectAmount rate is identical with and without splits.
|
|
767
|
-
function test_fork_invariant_tokenPerEthConsistent() public
|
|
726
|
+
function test_fork_invariant_tokenPerEthConsistent() public {
|
|
768
727
|
// --- Revnet 1: with 721 splits (30%) ---
|
|
769
728
|
(uint256 revnetId1, IJB721TiersHook hook1) = _deployRevnetWith721();
|
|
770
729
|
_setupPool(revnetId1, 10_000 ether);
|
|
@@ -30,7 +30,7 @@ import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/
|
|
|
30
30
|
|
|
31
31
|
/// @notice Tests for default operator permissions including SET_ROUTER_TERMINAL.
|
|
32
32
|
/// Verifies that all default split operator permissions are granted correctly.
|
|
33
|
-
contract
|
|
33
|
+
contract TestSwapTerminalPermission is TestBaseWorkflow {
|
|
34
34
|
bytes32 REV_DEPLOYER_SALT = "REVDeployer";
|
|
35
35
|
|
|
36
36
|
REVDeployer REV_DEPLOYER;
|
|
@@ -31,9 +31,9 @@ import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStor
|
|
|
31
31
|
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
32
32
|
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
33
33
|
|
|
34
|
-
/// @title
|
|
34
|
+
/// @title TestUint112Overflow
|
|
35
35
|
/// @notice Tests for uint112 truncation fix in REVLoans._adjust()
|
|
36
|
-
contract
|
|
36
|
+
contract TestUint112Overflow is TestBaseWorkflow {
|
|
37
37
|
bytes32 REV_DEPLOYER_SALT = "REVDeployer";
|
|
38
38
|
bytes32 ERC20_SALT = "REV_TOKEN";
|
|
39
39
|
|
|
@@ -233,7 +233,7 @@ contract TestPR21_Uint112Overflow is TestBaseWorkflow {
|
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
/// @notice Verify that uint112.max exactly does NOT revert (boundary test).
|
|
236
|
-
function test_boundaryValue_exactlyUint112Max() public {
|
|
236
|
+
function test_boundaryValue_exactlyUint112Max() public pure {
|
|
237
237
|
// uint112.max is the boundary — the check is `>`, not `>=`
|
|
238
238
|
// So exactly uint112.max should NOT revert
|
|
239
239
|
uint256 maxVal = type(uint112).max;
|
|
@@ -243,7 +243,7 @@ contract TestPR21_Uint112Overflow is TestBaseWorkflow {
|
|
|
243
243
|
|
|
244
244
|
/// @notice Verify the overflow check exists: values > uint112.max are rejected.
|
|
245
245
|
/// @dev We verify this by checking the error selector exists on the contract.
|
|
246
|
-
function test_overflowRevert_errorExists() public
|
|
246
|
+
function test_overflowRevert_errorExists() public pure {
|
|
247
247
|
// The fix adds REVLoans_OverflowAlert error. Verify the error exists
|
|
248
248
|
// by encoding it. If this compiles, the error exists.
|
|
249
249
|
bytes4 selector = REVLoans.REVLoans_OverflowAlert.selector;
|
|
@@ -32,7 +32,7 @@ import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressReg
|
|
|
32
32
|
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
33
33
|
|
|
34
34
|
/// @notice Tests for PR #16: zero repayment prevention.
|
|
35
|
-
contract
|
|
35
|
+
contract TestZeroRepayment is TestBaseWorkflow {
|
|
36
36
|
bytes32 REV_DEPLOYER_SALT = "REVDeployer";
|
|
37
37
|
|
|
38
38
|
REVDeployer REV_DEPLOYER;
|
|
@@ -212,8 +212,6 @@ contract TestPR16_ZeroRepayment is TestBaseWorkflow {
|
|
|
212
212
|
(uint256 loanId,,) = _setupLoan(USER, 10e18, 25);
|
|
213
213
|
require(loanId != 0, "Loan setup failed");
|
|
214
214
|
|
|
215
|
-
REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
|
|
216
|
-
|
|
217
215
|
// After borrowing, fees reduced surplus. We need to restore it so newBorrowAmount >= loan.amount.
|
|
218
216
|
// Donate enough surplus to compensate for all fees extracted during borrowing.
|
|
219
217
|
// A large donation ensures newBorrowAmount > loan.amount, hitting
|
|
@@ -243,7 +241,7 @@ contract TestPR16_ZeroRepayment is TestBaseWorkflow {
|
|
|
243
241
|
/// @notice Repaying full borrow amount should succeed (returning all collateral).
|
|
244
242
|
function test_repayNonZeroAmount_succeeds() public {
|
|
245
243
|
// Setup: borrow against 10 ETH
|
|
246
|
-
(uint256 loanId,,
|
|
244
|
+
(uint256 loanId,,) = _setupLoan(USER, 10e18, 25);
|
|
247
245
|
require(loanId != 0, "Loan setup failed");
|
|
248
246
|
|
|
249
247
|
REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
|
|
@@ -271,7 +269,7 @@ contract TestPR16_ZeroRepayment is TestBaseWorkflow {
|
|
|
271
269
|
/// @notice Repaying some collateral (non-zero) should succeed.
|
|
272
270
|
function test_repayNonZeroCollateral_succeeds() public {
|
|
273
271
|
// Setup: borrow against 10 ETH
|
|
274
|
-
(uint256 loanId,,
|
|
272
|
+
(uint256 loanId,,) = _setupLoan(USER, 10e18, 25);
|
|
275
273
|
require(loanId != 0, "Loan setup failed");
|
|
276
274
|
|
|
277
275
|
REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
|
|
@@ -283,7 +281,7 @@ contract TestPR16_ZeroRepayment is TestBaseWorkflow {
|
|
|
283
281
|
JBSingleAllowance memory allowance;
|
|
284
282
|
|
|
285
283
|
vm.prank(USER);
|
|
286
|
-
(
|
|
284
|
+
(, REVLoan memory paidOffLoan) = LOANS_CONTRACT.repayLoan{value: loan.amount}(
|
|
287
285
|
loanId,
|
|
288
286
|
loan.amount, // maxRepayBorrowAmount — generous cap
|
|
289
287
|
collateralToReturn,
|