@rev-net/core-v6 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ADMINISTRATION.md +186 -0
- package/ARCHITECTURE.md +87 -0
- package/README.md +4 -2
- package/RISKS.md +49 -0
- package/SKILLS.md +22 -2
- package/STYLE_GUIDE.md +482 -0
- package/foundry.toml +6 -6
- package/package.json +13 -10
- package/script/Deploy.s.sol +3 -2
- package/src/REVDeployer.sol +129 -72
- package/src/REVLoans.sol +174 -165
- package/src/interfaces/IREVDeployer.sol +111 -72
- package/src/interfaces/IREVLoans.sol +116 -76
- package/src/structs/REV721TiersHookFlags.sol +14 -0
- package/src/structs/REVBaseline721HookConfig.sol +27 -0
- package/src/structs/REVDeploy721TiersHookConfig.sol +2 -2
- package/test/REV.integrations.t.sol +4 -3
- package/test/REVAutoIssuanceFuzz.t.sol +12 -8
- package/test/REVDeployerAuditRegressions.t.sol +4 -3
- package/test/REVInvincibility.t.sol +8 -6
- package/test/REVInvincibilityHandler.sol +1 -0
- package/test/REVLifecycle.t.sol +4 -3
- package/test/REVLoans.invariants.t.sol +5 -3
- package/test/REVLoansAttacks.t.sol +4 -3
- package/test/REVLoansAuditRegressions.t.sol +13 -24
- package/test/REVLoansFeeRecovery.t.sol +4 -3
- package/test/REVLoansSourced.t.sol +4 -3
- package/test/REVLoansUnSourced.t.sol +4 -3
- package/test/REVLoans_AuditFindings.t.sol +644 -0
- package/test/TestEmptyBuybackSpecs.t.sol +4 -3
- package/test/TestPR09_ConversionDocumentation.t.sol +4 -3
- package/test/TestPR10_LiquidationBehavior.t.sol +4 -3
- package/test/TestPR11_LowFindings.t.sol +4 -3
- package/test/TestPR12_FlashLoanSurplus.t.sol +4 -3
- package/test/TestPR13_CrossSourceReallocation.t.sol +4 -3
- package/test/TestPR15_CashOutCallerValidation.t.sol +4 -3
- package/test/TestPR16_ZeroRepayment.t.sol +4 -3
- package/test/TestPR21_Uint112Overflow.t.sol +4 -3
- package/test/TestPR22_HookArrayOOB.t.sol +4 -3
- package/test/TestPR26_BurnHeldTokens.t.sol +4 -3
- package/test/TestPR27_CEIPattern.t.sol +4 -3
- package/test/TestPR29_SwapTerminalPermission.t.sol +4 -3
- package/test/TestPR32_MixedFixes.t.sol +4 -3
- package/test/TestSplitWeightAdjustment.t.sol +445 -0
- package/test/TestSplitWeightE2E.t.sol +528 -0
- package/test/TestSplitWeightFork.t.sol +821 -0
- package/test/TestStageTransitionBorrowable.t.sol +4 -3
- package/test/fork/ForkTestBase.sol +617 -0
- package/test/fork/TestCashOutFork.t.sol +245 -0
- package/test/fork/TestLoanBorrowFork.t.sol +163 -0
- package/test/fork/TestLoanLiquidationFork.t.sol +129 -0
- package/test/fork/TestLoanReallocateFork.t.sol +103 -0
- package/test/fork/TestLoanRepayFork.t.sol +184 -0
- package/test/fork/TestSplitWeightFork.t.sol +186 -0
- package/test/mock/MockBuybackDataHook.sol +11 -4
- package/test/mock/MockBuybackDataHookMintPath.sol +11 -3
- package/test/regression/TestI20_CumulativeLoanCounter.t.sol +6 -5
- package/test/regression/TestL27_LiquidateGapHandling.t.sol +7 -6
- package/SECURITY.md +0 -68
|
@@ -29,7 +29,7 @@ import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/
|
|
|
29
29
|
/// @notice Documents and verifies that stage transitions change the borrowable amount for the same collateral.
|
|
30
30
|
/// This is by design: loan value tracks the current bonding curve parameters (cashOutTaxRate),
|
|
31
31
|
/// just as cash-out value does.
|
|
32
|
-
contract TestStageTransitionBorrowable is TestBaseWorkflow
|
|
32
|
+
contract TestStageTransitionBorrowable is TestBaseWorkflow {
|
|
33
33
|
bytes32 REV_DEPLOYER_SALT = "REVDeployer";
|
|
34
34
|
|
|
35
35
|
REVDeployer REV_DEPLOYER;
|
|
@@ -110,7 +110,8 @@ contract TestStageTransitionBorrowable is TestBaseWorkflow, JBTest {
|
|
|
110
110
|
FEE_PROJECT_ID = jbProjects().createFor(multisig());
|
|
111
111
|
SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
|
|
112
112
|
HOOK_STORE = new JB721TiersHookStore();
|
|
113
|
-
EXAMPLE_HOOK =
|
|
113
|
+
EXAMPLE_HOOK =
|
|
114
|
+
new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, jbSplits(), multisig());
|
|
114
115
|
ADDRESS_REGISTRY = new JBAddressRegistry();
|
|
115
116
|
HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
|
|
116
117
|
PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
|
|
@@ -129,7 +130,7 @@ contract TestStageTransitionBorrowable is TestBaseWorkflow, JBTest {
|
|
|
129
130
|
FEE_PROJECT_ID,
|
|
130
131
|
HOOK_DEPLOYER,
|
|
131
132
|
PUBLISHER,
|
|
132
|
-
|
|
133
|
+
IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
|
|
133
134
|
address(LOANS_CONTRACT),
|
|
134
135
|
TRUSTED_FORWARDER
|
|
135
136
|
);
|
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
import "forge-std/Test.sol";
|
|
5
|
+
import /* {*} from */ "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
|
|
6
|
+
import /* {*} from */ "../../src/REVDeployer.sol";
|
|
7
|
+
import "@croptop/core-v6/src/CTPublisher.sol";
|
|
8
|
+
import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
|
|
9
|
+
import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
|
|
10
|
+
import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
|
|
11
|
+
import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
|
|
12
|
+
import "@bananapus/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
|
|
13
|
+
|
|
14
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
15
|
+
import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
|
|
16
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
17
|
+
import {REVLoans} from "../../src/REVLoans.sol";
|
|
18
|
+
import {REVLoan} from "../../src/structs/REVLoan.sol";
|
|
19
|
+
import {REVStageConfig, REVAutoIssuance} from "../../src/structs/REVStageConfig.sol";
|
|
20
|
+
import {REVLoanSource} from "../../src/structs/REVLoanSource.sol";
|
|
21
|
+
import {REVDescription} from "../../src/structs/REVDescription.sol";
|
|
22
|
+
import {IREVLoans} from "../../src/interfaces/IREVLoans.sol";
|
|
23
|
+
import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
|
|
24
|
+
import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
|
|
25
|
+
import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
|
|
26
|
+
import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
|
|
27
|
+
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
28
|
+
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
29
|
+
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
30
|
+
import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
|
|
31
|
+
import {IJBBuybackHookRegistry} from "@bananapus/buyback-hook-v6/src/interfaces/IJBBuybackHookRegistry.sol";
|
|
32
|
+
import {IJBPayHook} from "@bananapus/core-v6/src/interfaces/IJBPayHook.sol";
|
|
33
|
+
import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
|
|
34
|
+
import {JB721InitTiersConfig} from "@bananapus/721-hook-v6/src/structs/JB721InitTiersConfig.sol";
|
|
35
|
+
import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
|
|
36
|
+
import {REVDeploy721TiersHookConfig} from "../../src/structs/REVDeploy721TiersHookConfig.sol";
|
|
37
|
+
import {REVBaseline721HookConfig} from "../../src/structs/REVBaseline721HookConfig.sol";
|
|
38
|
+
import {REV721TiersHookFlags} from "../../src/structs/REV721TiersHookFlags.sol";
|
|
39
|
+
import {REVCroptopAllowedPost} from "../../src/structs/REVCroptopAllowedPost.sol";
|
|
40
|
+
|
|
41
|
+
// Buyback hook
|
|
42
|
+
import {JBBuybackHook} from "@bananapus/buyback-hook-v6/src/JBBuybackHook.sol";
|
|
43
|
+
import {JBBuybackHookRegistry} from "@bananapus/buyback-hook-v6/src/JBBuybackHookRegistry.sol";
|
|
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
|
+
import {IGeomeanOracle} from "@bananapus/buyback-hook-v6/src/interfaces/IGeomeanOracle.sol";
|
|
47
|
+
|
|
48
|
+
// Uniswap V4
|
|
49
|
+
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
|
|
50
|
+
import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
|
|
51
|
+
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
|
|
52
|
+
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
|
|
53
|
+
import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
|
|
54
|
+
import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
|
|
55
|
+
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
|
|
56
|
+
import {ModifyLiquidityParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
|
|
57
|
+
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
|
|
58
|
+
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
|
|
59
|
+
|
|
60
|
+
import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
|
|
61
|
+
|
|
62
|
+
/// @notice Helper that adds liquidity to a V4 pool via the unlock/callback pattern.
|
|
63
|
+
contract LiquidityHelper is IUnlockCallback {
|
|
64
|
+
IPoolManager public immutable poolManager;
|
|
65
|
+
|
|
66
|
+
struct AddLiqParams {
|
|
67
|
+
PoolKey key;
|
|
68
|
+
int24 tickLower;
|
|
69
|
+
int24 tickUpper;
|
|
70
|
+
int256 liquidityDelta;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
constructor(IPoolManager _poolManager) {
|
|
74
|
+
poolManager = _poolManager;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function addLiquidity(
|
|
78
|
+
PoolKey calldata key,
|
|
79
|
+
int24 tickLower,
|
|
80
|
+
int24 tickUpper,
|
|
81
|
+
int256 liquidityDelta
|
|
82
|
+
)
|
|
83
|
+
external
|
|
84
|
+
payable
|
|
85
|
+
{
|
|
86
|
+
bytes memory data = abi.encode(AddLiqParams(key, tickLower, tickUpper, liquidityDelta));
|
|
87
|
+
poolManager.unlock(data);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function unlockCallback(bytes calldata data) external override returns (bytes memory) {
|
|
91
|
+
require(msg.sender == address(poolManager), "only PM");
|
|
92
|
+
|
|
93
|
+
AddLiqParams memory params = abi.decode(data, (AddLiqParams));
|
|
94
|
+
|
|
95
|
+
(BalanceDelta callerDelta,) = poolManager.modifyLiquidity(
|
|
96
|
+
params.key,
|
|
97
|
+
ModifyLiquidityParams({
|
|
98
|
+
tickLower: params.tickLower,
|
|
99
|
+
tickUpper: params.tickUpper,
|
|
100
|
+
liquidityDelta: params.liquidityDelta,
|
|
101
|
+
salt: bytes32(0)
|
|
102
|
+
}),
|
|
103
|
+
""
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
_settleIfNegative(params.key.currency0, callerDelta.amount0());
|
|
107
|
+
_settleIfNegative(params.key.currency1, callerDelta.amount1());
|
|
108
|
+
_takeIfPositive(params.key.currency0, callerDelta.amount0());
|
|
109
|
+
_takeIfPositive(params.key.currency1, callerDelta.amount1());
|
|
110
|
+
|
|
111
|
+
return abi.encode(callerDelta);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function _settleIfNegative(Currency currency, int128 delta) internal {
|
|
115
|
+
if (delta >= 0) return;
|
|
116
|
+
uint256 amount = uint256(uint128(-delta));
|
|
117
|
+
|
|
118
|
+
if (currency.isAddressZero()) {
|
|
119
|
+
poolManager.settle{value: amount}();
|
|
120
|
+
} else {
|
|
121
|
+
poolManager.sync(currency);
|
|
122
|
+
IERC20(Currency.unwrap(currency)).transfer(address(poolManager), amount);
|
|
123
|
+
poolManager.settle();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function _takeIfPositive(Currency currency, int128 delta) internal {
|
|
128
|
+
if (delta <= 0) return;
|
|
129
|
+
uint256 amount = uint256(uint128(delta));
|
|
130
|
+
poolManager.take(currency, address(this), amount);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
receive() external payable {}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// @notice Shared base for fork tests. Deploys full JB core + REVDeployer infrastructure on a mainnet fork.
|
|
137
|
+
///
|
|
138
|
+
/// Requires: RPC_ETHEREUM_MAINNET env var for mainnet fork (real PoolManager).
|
|
139
|
+
abstract contract ForkTestBase is TestBaseWorkflow {
|
|
140
|
+
using JBMetadataResolver for bytes;
|
|
141
|
+
using PoolIdLibrary for PoolKey;
|
|
142
|
+
using CurrencyLibrary for Currency;
|
|
143
|
+
using StateLibrary for IPoolManager;
|
|
144
|
+
|
|
145
|
+
// ───────────────────────── Mainnet constants
|
|
146
|
+
// ─────────────────────────
|
|
147
|
+
|
|
148
|
+
address constant POOL_MANAGER_ADDR = 0x000000000004444c5dc75cB358380D2e3dE08A90;
|
|
149
|
+
address constant WETH_ADDR = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
|
|
150
|
+
|
|
151
|
+
/// @notice Full-range tick bounds for tickSpacing = 60.
|
|
152
|
+
int24 constant TICK_LOWER = -887_220;
|
|
153
|
+
int24 constant TICK_UPPER = 887_220;
|
|
154
|
+
|
|
155
|
+
// ───────────────────────── State
|
|
156
|
+
// ─────────────────────────
|
|
157
|
+
|
|
158
|
+
REVDeployer REV_DEPLOYER;
|
|
159
|
+
JBBuybackHook BUYBACK_HOOK;
|
|
160
|
+
JBBuybackHookRegistry BUYBACK_REGISTRY;
|
|
161
|
+
JB721TiersHook EXAMPLE_HOOK;
|
|
162
|
+
IJB721TiersHookDeployer HOOK_DEPLOYER;
|
|
163
|
+
IJB721TiersHookStore HOOK_STORE;
|
|
164
|
+
IJBAddressRegistry ADDRESS_REGISTRY;
|
|
165
|
+
IREVLoans LOANS_CONTRACT;
|
|
166
|
+
IJBSuckerRegistry SUCKER_REGISTRY;
|
|
167
|
+
CTPublisher PUBLISHER;
|
|
168
|
+
IPoolManager poolManager;
|
|
169
|
+
IWETH9 weth;
|
|
170
|
+
LiquidityHelper liqHelper;
|
|
171
|
+
|
|
172
|
+
uint256 FEE_PROJECT_ID;
|
|
173
|
+
|
|
174
|
+
address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
|
|
175
|
+
address PAYER = makeAddr("payer");
|
|
176
|
+
address BORROWER = makeAddr("borrower");
|
|
177
|
+
address SPLIT_BENEFICIARY = makeAddr("splitBeneficiary");
|
|
178
|
+
|
|
179
|
+
// Tier configuration: 1 ETH tier with 30% split.
|
|
180
|
+
uint104 constant TIER_PRICE = 1 ether;
|
|
181
|
+
uint32 constant SPLIT_PERCENT = 300_000_000; // 30% of SPLITS_TOTAL_PERCENT (1_000_000_000)
|
|
182
|
+
uint112 constant INITIAL_ISSUANCE = 1000e18; // 1000 tokens per ETH
|
|
183
|
+
|
|
184
|
+
// ───────────────────────── Setup
|
|
185
|
+
// ─────────────────────────
|
|
186
|
+
|
|
187
|
+
function setUp() public virtual override {
|
|
188
|
+
// Fork mainnet first — we need the real V4 PoolManager.
|
|
189
|
+
string memory rpcUrl = vm.envOr("RPC_ETHEREUM_MAINNET", string(""));
|
|
190
|
+
if (bytes(rpcUrl).length == 0) {
|
|
191
|
+
vm.skip(true);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
vm.createSelectFork(rpcUrl);
|
|
195
|
+
|
|
196
|
+
// Verify V4 PoolManager is deployed.
|
|
197
|
+
require(POOL_MANAGER_ADDR.code.length > 0, "PoolManager not deployed at expected address");
|
|
198
|
+
|
|
199
|
+
// Deploy fresh JB core on the forked mainnet.
|
|
200
|
+
super.setUp();
|
|
201
|
+
|
|
202
|
+
poolManager = IPoolManager(POOL_MANAGER_ADDR);
|
|
203
|
+
weth = IWETH9(WETH_ADDR);
|
|
204
|
+
liqHelper = new LiquidityHelper(poolManager);
|
|
205
|
+
|
|
206
|
+
FEE_PROJECT_ID = jbProjects().createFor(multisig());
|
|
207
|
+
|
|
208
|
+
SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
|
|
209
|
+
HOOK_STORE = new JB721TiersHookStore();
|
|
210
|
+
EXAMPLE_HOOK =
|
|
211
|
+
new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, jbSplits(), multisig());
|
|
212
|
+
ADDRESS_REGISTRY = new JBAddressRegistry();
|
|
213
|
+
HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
|
|
214
|
+
PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
|
|
215
|
+
|
|
216
|
+
// Deploy REAL buyback hook with real PoolManager.
|
|
217
|
+
BUYBACK_HOOK = new JBBuybackHook(
|
|
218
|
+
jbDirectory(),
|
|
219
|
+
jbPermissions(),
|
|
220
|
+
jbPrices(),
|
|
221
|
+
jbProjects(),
|
|
222
|
+
jbTokens(),
|
|
223
|
+
weth,
|
|
224
|
+
poolManager,
|
|
225
|
+
address(0) // trustedForwarder
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// Deploy the registry and set the buyback hook as the default.
|
|
229
|
+
BUYBACK_REGISTRY = new JBBuybackHookRegistry(
|
|
230
|
+
jbPermissions(),
|
|
231
|
+
jbProjects(),
|
|
232
|
+
address(this), // owner
|
|
233
|
+
address(0) // trustedForwarder
|
|
234
|
+
);
|
|
235
|
+
BUYBACK_REGISTRY.setDefaultHook(IJBRulesetDataHook(address(BUYBACK_HOOK)));
|
|
236
|
+
|
|
237
|
+
LOANS_CONTRACT = new REVLoans({
|
|
238
|
+
controller: jbController(),
|
|
239
|
+
projects: jbProjects(),
|
|
240
|
+
revId: FEE_PROJECT_ID,
|
|
241
|
+
owner: address(this),
|
|
242
|
+
permit2: permit2(),
|
|
243
|
+
trustedForwarder: TRUSTED_FORWARDER
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
REV_DEPLOYER = new REVDeployer{salt: "REVDeployer_Fork"}(
|
|
247
|
+
jbController(),
|
|
248
|
+
SUCKER_REGISTRY,
|
|
249
|
+
FEE_PROJECT_ID,
|
|
250
|
+
HOOK_DEPLOYER,
|
|
251
|
+
PUBLISHER,
|
|
252
|
+
IJBBuybackHookRegistry(address(BUYBACK_REGISTRY)),
|
|
253
|
+
address(LOANS_CONTRACT),
|
|
254
|
+
TRUSTED_FORWARDER
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
vm.prank(multisig());
|
|
258
|
+
jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
|
|
259
|
+
|
|
260
|
+
// Fund payer and borrower.
|
|
261
|
+
vm.deal(PAYER, 100 ether);
|
|
262
|
+
vm.deal(BORROWER, 100 ether);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
modifier onlyFork() {
|
|
266
|
+
string memory rpcUrl = vm.envOr("RPC_ETHEREUM_MAINNET", string(""));
|
|
267
|
+
if (bytes(rpcUrl).length == 0) return;
|
|
268
|
+
_;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ───────────────────────── Config Helpers
|
|
272
|
+
// ─────────────────────────
|
|
273
|
+
|
|
274
|
+
function _buildMinimalConfig(uint16 cashOutTaxRate)
|
|
275
|
+
internal
|
|
276
|
+
view
|
|
277
|
+
returns (REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc)
|
|
278
|
+
{
|
|
279
|
+
JBAccountingContext[] memory acc = new JBAccountingContext[](1);
|
|
280
|
+
acc[0] = JBAccountingContext({
|
|
281
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
282
|
+
});
|
|
283
|
+
tc = new JBTerminalConfig[](1);
|
|
284
|
+
tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
|
|
285
|
+
|
|
286
|
+
REVStageConfig[] memory stages = new REVStageConfig[](1);
|
|
287
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
288
|
+
splits[0].beneficiary = payable(multisig());
|
|
289
|
+
splits[0].percent = 10_000;
|
|
290
|
+
stages[0] = REVStageConfig({
|
|
291
|
+
startsAtOrAfter: uint40(block.timestamp),
|
|
292
|
+
autoIssuances: new REVAutoIssuance[](0),
|
|
293
|
+
splitPercent: 0,
|
|
294
|
+
splits: splits,
|
|
295
|
+
initialIssuance: INITIAL_ISSUANCE,
|
|
296
|
+
issuanceCutFrequency: 0,
|
|
297
|
+
issuanceCutPercent: 0,
|
|
298
|
+
cashOutTaxRate: cashOutTaxRate,
|
|
299
|
+
extraMetadata: 0
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
cfg = REVConfig({
|
|
303
|
+
description: REVDescription("Fork Test", "FORK", "ipfs://fork", "FORK_SALT"),
|
|
304
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
305
|
+
splitOperator: multisig(),
|
|
306
|
+
stageConfigurations: stages
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
sdc = REVSuckerDeploymentConfig({
|
|
310
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("FORK_TEST"))
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function _build721Config() internal view returns (REVDeploy721TiersHookConfig memory) {
|
|
315
|
+
JB721TierConfig[] memory tiers = new JB721TierConfig[](1);
|
|
316
|
+
JBSplit[] memory tierSplits = new JBSplit[](1);
|
|
317
|
+
tierSplits[0] = JBSplit({
|
|
318
|
+
preferAddToBalance: false,
|
|
319
|
+
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
|
|
320
|
+
projectId: 0,
|
|
321
|
+
beneficiary: payable(SPLIT_BENEFICIARY),
|
|
322
|
+
lockedUntil: 0,
|
|
323
|
+
hook: IJBSplitHook(address(0))
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
tiers[0] = JB721TierConfig({
|
|
327
|
+
price: TIER_PRICE,
|
|
328
|
+
initialSupply: 100,
|
|
329
|
+
votingUnits: 0,
|
|
330
|
+
reserveFrequency: 0,
|
|
331
|
+
reserveBeneficiary: address(0),
|
|
332
|
+
encodedIPFSUri: bytes32("tier1"),
|
|
333
|
+
category: 1,
|
|
334
|
+
discountPercent: 0,
|
|
335
|
+
allowOwnerMint: false,
|
|
336
|
+
useReserveBeneficiaryAsDefault: false,
|
|
337
|
+
transfersPausable: false,
|
|
338
|
+
useVotingUnits: false,
|
|
339
|
+
cannotBeRemoved: false,
|
|
340
|
+
cannotIncreaseDiscountPercent: false,
|
|
341
|
+
splitPercent: SPLIT_PERCENT,
|
|
342
|
+
splits: tierSplits
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
return REVDeploy721TiersHookConfig({
|
|
346
|
+
baseline721HookConfiguration: REVBaseline721HookConfig({
|
|
347
|
+
name: "Fork NFT",
|
|
348
|
+
symbol: "FNFT",
|
|
349
|
+
baseUri: "ipfs://",
|
|
350
|
+
tokenUriResolver: IJB721TokenUriResolver(address(0)),
|
|
351
|
+
contractUri: "ipfs://contract",
|
|
352
|
+
tiersConfig: JB721InitTiersConfig({
|
|
353
|
+
tiers: tiers,
|
|
354
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
355
|
+
decimals: 18,
|
|
356
|
+
prices: IJBPrices(address(0))
|
|
357
|
+
}),
|
|
358
|
+
reserveBeneficiary: address(0),
|
|
359
|
+
flags: REV721TiersHookFlags({
|
|
360
|
+
noNewTiersWithReserves: false,
|
|
361
|
+
noNewTiersWithVotes: false,
|
|
362
|
+
noNewTiersWithOwnerMinting: false,
|
|
363
|
+
preventOverspending: false
|
|
364
|
+
})
|
|
365
|
+
}),
|
|
366
|
+
salt: bytes32("FORK_721"),
|
|
367
|
+
splitOperatorCanAdjustTiers: false,
|
|
368
|
+
splitOperatorCanUpdateMetadata: false,
|
|
369
|
+
splitOperatorCanMint: false,
|
|
370
|
+
splitOperatorCanIncreaseDiscountPercent: false
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ───────────────────────── Deployment Helpers
|
|
375
|
+
// ─────────────────────────
|
|
376
|
+
|
|
377
|
+
/// @notice Deploy the fee project using the given tax rate.
|
|
378
|
+
function _deployFeeProject(uint16 cashOutTaxRate) internal {
|
|
379
|
+
(REVConfig memory feeCfg, JBTerminalConfig[] memory feeTc, REVSuckerDeploymentConfig memory feeSdc) =
|
|
380
|
+
_buildMinimalConfig(cashOutTaxRate);
|
|
381
|
+
feeCfg.description = REVDescription("Fee", "FEE", "ipfs://fee", "FEE_SALT");
|
|
382
|
+
|
|
383
|
+
vm.prank(multisig());
|
|
384
|
+
REV_DEPLOYER.deployFor({
|
|
385
|
+
revnetId: FEE_PROJECT_ID,
|
|
386
|
+
configuration: feeCfg,
|
|
387
|
+
terminalConfigurations: feeTc,
|
|
388
|
+
suckerDeploymentConfiguration: feeSdc
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/// @notice Deploy a revnet (no 721 hook) with the given cash out tax rate.
|
|
393
|
+
function _deployRevnet(uint16 cashOutTaxRate) internal returns (uint256 revnetId) {
|
|
394
|
+
(REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc) =
|
|
395
|
+
_buildMinimalConfig(cashOutTaxRate);
|
|
396
|
+
|
|
397
|
+
revnetId = REV_DEPLOYER.deployFor({
|
|
398
|
+
revnetId: 0, configuration: cfg, terminalConfigurations: tc, suckerDeploymentConfiguration: sdc
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/// @notice Deploy a revnet with 721 tiers and the given cash out tax rate.
|
|
403
|
+
function _deployRevnetWith721(uint16 cashOutTaxRate) internal returns (uint256 revnetId, IJB721TiersHook hook) {
|
|
404
|
+
(REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc) =
|
|
405
|
+
_buildMinimalConfig(cashOutTaxRate);
|
|
406
|
+
REVDeploy721TiersHookConfig memory hookConfig = _build721Config();
|
|
407
|
+
|
|
408
|
+
(revnetId, hook) = REV_DEPLOYER.deployWith721sFor({
|
|
409
|
+
revnetId: 0,
|
|
410
|
+
configuration: cfg,
|
|
411
|
+
terminalConfigurations: tc,
|
|
412
|
+
suckerDeploymentConfiguration: sdc,
|
|
413
|
+
tiered721HookConfiguration: hookConfig,
|
|
414
|
+
allowedPosts: new REVCroptopAllowedPost[](0)
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ───────────────────────── Pool Helpers
|
|
419
|
+
// ─────────────────────────
|
|
420
|
+
|
|
421
|
+
/// @notice Set up a V4 pool for the revnet's project token / WETH pair at 1:1 price.
|
|
422
|
+
function _setupPool(uint256 revnetId, uint256 liquidityTokenAmount) internal returns (PoolKey memory key) {
|
|
423
|
+
address projectToken = address(jbTokens().tokenOf(revnetId));
|
|
424
|
+
require(projectToken != address(0), "project token not deployed");
|
|
425
|
+
|
|
426
|
+
address token0;
|
|
427
|
+
address token1;
|
|
428
|
+
if (projectToken < WETH_ADDR) {
|
|
429
|
+
token0 = projectToken;
|
|
430
|
+
token1 = WETH_ADDR;
|
|
431
|
+
} else {
|
|
432
|
+
token0 = WETH_ADDR;
|
|
433
|
+
token1 = projectToken;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
key = PoolKey({
|
|
437
|
+
currency0: Currency.wrap(token0),
|
|
438
|
+
currency1: Currency.wrap(token1),
|
|
439
|
+
fee: REV_DEPLOYER.DEFAULT_BUYBACK_POOL_FEE(),
|
|
440
|
+
tickSpacing: REV_DEPLOYER.DEFAULT_BUYBACK_TICK_SPACING(),
|
|
441
|
+
hooks: IHooks(address(0))
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
uint160 sqrtPrice = TickMath.getSqrtPriceAtTick(0);
|
|
445
|
+
poolManager.initialize(key, sqrtPrice);
|
|
446
|
+
|
|
447
|
+
// Fund LiquidityHelper with project tokens via JBTokens.mintFor (not deal).
|
|
448
|
+
vm.prank(address(jbController()));
|
|
449
|
+
jbTokens().mintFor(address(liqHelper), revnetId, liquidityTokenAmount);
|
|
450
|
+
vm.deal(address(liqHelper), liquidityTokenAmount);
|
|
451
|
+
vm.prank(address(liqHelper));
|
|
452
|
+
IWETH9(WETH_ADDR).deposit{value: liquidityTokenAmount}();
|
|
453
|
+
|
|
454
|
+
vm.startPrank(address(liqHelper));
|
|
455
|
+
IERC20(projectToken).approve(address(poolManager), type(uint256).max);
|
|
456
|
+
IERC20(WETH_ADDR).approve(address(poolManager), type(uint256).max);
|
|
457
|
+
vm.stopPrank();
|
|
458
|
+
|
|
459
|
+
int256 liquidityDelta = int256(liquidityTokenAmount / 2);
|
|
460
|
+
vm.prank(address(liqHelper));
|
|
461
|
+
liqHelper.addLiquidity(key, TICK_LOWER, TICK_UPPER, liquidityDelta);
|
|
462
|
+
|
|
463
|
+
_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
|
+
}
|
|
471
|
+
|
|
472
|
+
/// @notice Mock the IGeomeanOracle at address(0) for hookless pools.
|
|
473
|
+
function _mockOracle(int256 liquidity, int24 tick, uint32 twapWindow) internal {
|
|
474
|
+
vm.etch(address(0), hex"00");
|
|
475
|
+
|
|
476
|
+
int56[] memory tickCumulatives = new int56[](2);
|
|
477
|
+
tickCumulatives[0] = 0;
|
|
478
|
+
tickCumulatives[1] = int56(tick) * int56(int32(twapWindow));
|
|
479
|
+
|
|
480
|
+
uint160[] memory secondsPerLiquidityCumulativeX128s = new uint160[](2);
|
|
481
|
+
secondsPerLiquidityCumulativeX128s[0] = 0;
|
|
482
|
+
uint256 liq = uint256(liquidity > 0 ? liquidity : -liquidity);
|
|
483
|
+
if (liq == 0) liq = 1;
|
|
484
|
+
secondsPerLiquidityCumulativeX128s[1] = uint160((uint256(twapWindow) << 128) / liq);
|
|
485
|
+
|
|
486
|
+
vm.mockCall(
|
|
487
|
+
address(0),
|
|
488
|
+
abi.encodeWithSelector(IGeomeanOracle.observe.selector),
|
|
489
|
+
abi.encode(tickCumulatives, secondsPerLiquidityCumulativeX128s)
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ───────────────────────── Balance Helpers
|
|
494
|
+
// ─────────────────────────
|
|
495
|
+
|
|
496
|
+
/// @notice Get a project's balance in the terminal store.
|
|
497
|
+
function _terminalBalance(uint256 projectId, address token) internal view returns (uint256) {
|
|
498
|
+
return jbTerminalStore().balanceOf(address(jbMultiTerminal()), projectId, token);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ───────────────────────── Payment Helpers
|
|
502
|
+
// ─────────────────────────
|
|
503
|
+
|
|
504
|
+
/// @notice Pay the revnet with ETH.
|
|
505
|
+
function _payRevnet(uint256 revnetId, address payer, uint256 amount) internal returns (uint256 tokensReceived) {
|
|
506
|
+
vm.prank(payer);
|
|
507
|
+
tokensReceived = jbMultiTerminal().pay{value: amount}({
|
|
508
|
+
projectId: revnetId,
|
|
509
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
510
|
+
amount: amount,
|
|
511
|
+
beneficiary: payer,
|
|
512
|
+
minReturnedTokens: 0,
|
|
513
|
+
memo: "",
|
|
514
|
+
metadata: ""
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ───────────────────────── Metadata Helpers
|
|
519
|
+
// ─────────────────────────
|
|
520
|
+
|
|
521
|
+
/// @notice Build payment metadata with both 721 tier selection AND buyback quote.
|
|
522
|
+
function _buildPayMetadataWithQuote(
|
|
523
|
+
address hookMetadataTarget,
|
|
524
|
+
uint256 amountToSwapWith,
|
|
525
|
+
uint256 minimumSwapAmountOut
|
|
526
|
+
)
|
|
527
|
+
internal
|
|
528
|
+
view
|
|
529
|
+
returns (bytes memory)
|
|
530
|
+
{
|
|
531
|
+
uint16[] memory tierIds = new uint16[](1);
|
|
532
|
+
tierIds[0] = 1;
|
|
533
|
+
bytes memory tierData = abi.encode(true, tierIds);
|
|
534
|
+
bytes4 tierMetadataId = JBMetadataResolver.getId("pay", hookMetadataTarget);
|
|
535
|
+
|
|
536
|
+
bytes memory quoteData = abi.encode(amountToSwapWith, minimumSwapAmountOut);
|
|
537
|
+
bytes4 quoteMetadataId = JBMetadataResolver.getId("quote");
|
|
538
|
+
|
|
539
|
+
bytes4[] memory ids = new bytes4[](2);
|
|
540
|
+
ids[0] = tierMetadataId;
|
|
541
|
+
ids[1] = quoteMetadataId;
|
|
542
|
+
bytes[] memory datas = new bytes[](2);
|
|
543
|
+
datas[0] = tierData;
|
|
544
|
+
datas[1] = quoteData;
|
|
545
|
+
|
|
546
|
+
return JBMetadataResolver.createMetadata(ids, datas);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/// @notice Build payment metadata with only 721 tier selection (no quote -> TWAP/spot fallback).
|
|
550
|
+
function _buildPayMetadataNoQuote(address hookMetadataTarget) internal view returns (bytes memory) {
|
|
551
|
+
uint16[] memory tierIds = new uint16[](1);
|
|
552
|
+
tierIds[0] = 1;
|
|
553
|
+
bytes memory tierData = abi.encode(true, tierIds);
|
|
554
|
+
bytes4 tierMetadataId = JBMetadataResolver.getId("pay", hookMetadataTarget);
|
|
555
|
+
|
|
556
|
+
bytes4[] memory ids = new bytes4[](1);
|
|
557
|
+
ids[0] = tierMetadataId;
|
|
558
|
+
bytes[] memory datas = new bytes[](1);
|
|
559
|
+
datas[0] = tierData;
|
|
560
|
+
|
|
561
|
+
return JBMetadataResolver.createMetadata(ids, datas);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// ───────────────────────── ERC721 Helpers
|
|
565
|
+
// ─────────────────────────
|
|
566
|
+
|
|
567
|
+
/// @notice Get the owner of a loan NFT. REVLoans is ERC721 but typed as IREVLoans.
|
|
568
|
+
function _loanOwnerOf(uint256 loanId) internal view returns (address) {
|
|
569
|
+
return REVLoans(payable(address(LOANS_CONTRACT))).ownerOf(loanId);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// ───────────────────────── Loan Helpers
|
|
573
|
+
// ─────────────────────────
|
|
574
|
+
|
|
575
|
+
/// @notice Build a native token loan source.
|
|
576
|
+
function _nativeLoanSource() internal view returns (REVLoanSource memory) {
|
|
577
|
+
return REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/// @notice Grant BURN_TOKENS permission to the loans contract for a given account.
|
|
581
|
+
function _grantBurnPermission(address account, uint256 revnetId) internal {
|
|
582
|
+
// Permission ID 11 = BURN_TOKENS in JBPermissionIds
|
|
583
|
+
mockExpect(
|
|
584
|
+
address(jbPermissions()),
|
|
585
|
+
abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), account, revnetId, 11, true, true)),
|
|
586
|
+
abi.encode(true)
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/// @notice Create a loan for the given borrower. Returns the loan ID and loan struct.
|
|
591
|
+
function _createLoan(
|
|
592
|
+
uint256 revnetId,
|
|
593
|
+
address borrower,
|
|
594
|
+
uint256 collateral,
|
|
595
|
+
uint256 prepaidFeePercent
|
|
596
|
+
)
|
|
597
|
+
internal
|
|
598
|
+
returns (uint256 loanId, REVLoan memory loan)
|
|
599
|
+
{
|
|
600
|
+
REVLoanSource memory source = _nativeLoanSource();
|
|
601
|
+
uint256 borrowable =
|
|
602
|
+
LOANS_CONTRACT.borrowableAmountFrom(revnetId, collateral, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
603
|
+
require(borrowable > 0, "no borrowable amount");
|
|
604
|
+
|
|
605
|
+
_grantBurnPermission(borrower, revnetId);
|
|
606
|
+
|
|
607
|
+
vm.prank(borrower);
|
|
608
|
+
(loanId, loan) = LOANS_CONTRACT.borrowFrom({
|
|
609
|
+
revnetId: revnetId,
|
|
610
|
+
source: source,
|
|
611
|
+
minBorrowAmount: 0,
|
|
612
|
+
collateralCount: collateral,
|
|
613
|
+
beneficiary: payable(borrower),
|
|
614
|
+
prepaidFeePercent: prepaidFeePercent
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}
|