@venusprotocol/venus-periphery 1.2.0-dev.6 → 1.2.0-dev.7
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.
|
@@ -0,0 +1,1302 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.main = exports.orchestrate = exports.exportJson = exports.generateCommands = exports.gatherInput = exports.parsePerPoolValuesJson = exports.parsePerMarketValuesJson = exports.parsePoolIdList = exports.getEBrakeAddress = exports.parseUint256String = exports.buildSafeBatch = exports.ALLOWED_PAUSE_ACTIONS = exports.retry = void 0;
|
|
27
|
+
const fs = __importStar(require("fs"));
|
|
28
|
+
const hardhat_1 = require("hardhat");
|
|
29
|
+
const path = __importStar(require("path"));
|
|
30
|
+
const readline = __importStar(require("readline"));
|
|
31
|
+
// ─── Terminal colors ─────────────────────────────────────────────────────────
|
|
32
|
+
const colorEnabled = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
33
|
+
const colorWrap = (code) => (s) => colorEnabled ? `\x1b[${code}m${s}\x1b[0m` : s;
|
|
34
|
+
const red = colorWrap("31");
|
|
35
|
+
const green = colorWrap("32");
|
|
36
|
+
const yellow = colorWrap("33");
|
|
37
|
+
const cyan = colorWrap("36");
|
|
38
|
+
const bold = colorWrap("1");
|
|
39
|
+
// ─── Retry helper ────────────────────────────────────────────────────────────
|
|
40
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
+
async function retry(fn, retries = 3, delayMs = 1000) {
|
|
43
|
+
let lastError;
|
|
44
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
45
|
+
try {
|
|
46
|
+
return await fn();
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
lastError = error;
|
|
50
|
+
if (attempt < retries) {
|
|
51
|
+
console.log(` RPC call failed (attempt ${attempt}/${retries}), retrying in ${delayMs / 1000}s...`);
|
|
52
|
+
await sleep(delayMs);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
throw lastError;
|
|
57
|
+
}
|
|
58
|
+
exports.retry = retry;
|
|
59
|
+
// ─── Configuration ──────────────────────────────────────────────────────────
|
|
60
|
+
const OUTPUT_DIR = path.resolve(__dirname, "data");
|
|
61
|
+
const EBRAKE_ABI = [
|
|
62
|
+
"function IS_ISOLATED_POOL() view returns (bool)",
|
|
63
|
+
"function COMPTROLLER() view returns (address)",
|
|
64
|
+
"function pauseActions(address[],uint8[])",
|
|
65
|
+
"function pauseSupply(address)",
|
|
66
|
+
"function pauseRedeem(address)",
|
|
67
|
+
"function pauseBorrow(address)",
|
|
68
|
+
"function pauseTransfer(address)",
|
|
69
|
+
"function pauseFlashLoan()",
|
|
70
|
+
"function disablePoolBorrow(uint96,address)",
|
|
71
|
+
"function revokeFlashLoanAccess(address)",
|
|
72
|
+
"function decreaseCF(address,uint256)",
|
|
73
|
+
"function decreaseCF(address,uint96,uint256)",
|
|
74
|
+
"function setMarketBorrowCaps(address[],uint256[])",
|
|
75
|
+
"function setMarketSupplyCaps(address[],uint256[])",
|
|
76
|
+
];
|
|
77
|
+
// ABIs used for on-chain queries (current CF / caps display)
|
|
78
|
+
const BSC_COMPTROLLER_QUERY_ABI = [
|
|
79
|
+
"function getAllMarkets() view returns (address[])",
|
|
80
|
+
"function markets(address) view returns (bool isListed, uint256 collateralFactorMantissa, bool isVenus, uint256 liquidationThresholdMantissa, uint256 liquidationIncentiveMantissa, uint96 marketPoolId, bool isBorrowAllowed)",
|
|
81
|
+
"function poolMarkets(uint96,address) view returns (bool isListed, uint256 collateralFactorMantissa, bool isVenus, uint256 liquidationThresholdMantissa, uint256 liquidationIncentiveMantissa, uint96 marketPoolId, bool isBorrowAllowed)",
|
|
82
|
+
"function corePoolId() view returns (uint96)",
|
|
83
|
+
"function lastPoolId() view returns (uint96)",
|
|
84
|
+
"function borrowCaps(address) view returns (uint256)",
|
|
85
|
+
"function supplyCaps(address) view returns (uint256)",
|
|
86
|
+
];
|
|
87
|
+
const IL_COMPTROLLER_QUERY_ABI = [
|
|
88
|
+
"function getAllMarkets() view returns (address[])",
|
|
89
|
+
"function markets(address) view returns (bool isListed, uint256 collateralFactorMantissa, uint256 liquidationThresholdMantissa)",
|
|
90
|
+
"function borrowCaps(address) view returns (uint256)",
|
|
91
|
+
"function supplyCaps(address) view returns (uint256)",
|
|
92
|
+
];
|
|
93
|
+
// Only MINT, REDEEM, BORROW, TRANSFER are allowed by EBrake (REPAY/SEIZE/LIQUIDATE are forbidden)
|
|
94
|
+
exports.ALLOWED_PAUSE_ACTIONS = {
|
|
95
|
+
MINT: 0,
|
|
96
|
+
REDEEM: 1,
|
|
97
|
+
BORROW: 2,
|
|
98
|
+
TRANSFER: 6,
|
|
99
|
+
};
|
|
100
|
+
// ─── Safe TX Builder JSON ────────────────────────────────────────────────────
|
|
101
|
+
const buildSafeBatch = (safeAddress, transactions, chainId, blockNumber) => {
|
|
102
|
+
const createdAt = Date.now();
|
|
103
|
+
const txs = transactions.map(tx => ({
|
|
104
|
+
to: tx.to,
|
|
105
|
+
value: tx.value,
|
|
106
|
+
data: tx.data,
|
|
107
|
+
contractMethod: null,
|
|
108
|
+
contractInputsValues: null,
|
|
109
|
+
}));
|
|
110
|
+
const metaBase = {
|
|
111
|
+
name: "Transactions Batch",
|
|
112
|
+
description: "",
|
|
113
|
+
txBuilderVersion: "1.8.0",
|
|
114
|
+
createdFromSafeAddress: safeAddress,
|
|
115
|
+
createdFromOwnerAddress: "",
|
|
116
|
+
};
|
|
117
|
+
// Checksum matches the format expected by the Safe TX Builder UI
|
|
118
|
+
const checksum = hardhat_1.ethers.utils.id(JSON.stringify({ version: "1.0", chainId: chainId.toString(), createdAt, meta: metaBase, transactions: txs }));
|
|
119
|
+
return {
|
|
120
|
+
version: "1.0",
|
|
121
|
+
chainId: chainId.toString(),
|
|
122
|
+
createdAt,
|
|
123
|
+
meta: { ...metaBase, checksum },
|
|
124
|
+
transactions: txs,
|
|
125
|
+
blockNumber,
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
exports.buildSafeBatch = buildSafeBatch;
|
|
129
|
+
// ─── CLI helpers ─────────────────────────────────────────────────────────────
|
|
130
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
131
|
+
const ask = (question) => new Promise(resolve => rl.question(question, answer => resolve(answer.trim())));
|
|
132
|
+
const pickOne = async (prompt, options) => {
|
|
133
|
+
console.log(`\n${prompt}`);
|
|
134
|
+
options.forEach((opt, i) => console.log(` ${i + 1}. ${opt}`));
|
|
135
|
+
// eslint-disable-next-line no-constant-condition
|
|
136
|
+
while (true) {
|
|
137
|
+
const answer = await ask("Enter number: ");
|
|
138
|
+
const idx = parseInt(answer, 10) - 1;
|
|
139
|
+
if (idx >= 0 && idx < options.length) {
|
|
140
|
+
return options[idx];
|
|
141
|
+
}
|
|
142
|
+
console.log(red(`Invalid selection "${answer}". Please enter a number between 1 and ${options.length}.`));
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const pickMultiple = async (prompt, options, { allowAll = true, preserveOrder = false } = {}) => {
|
|
146
|
+
console.log(`\n${prompt}`);
|
|
147
|
+
options.forEach(opt => console.log(` ${opt.value}. ${opt.name}`));
|
|
148
|
+
const validValues = new Set(options.map(o => o.value));
|
|
149
|
+
// eslint-disable-next-line no-constant-condition
|
|
150
|
+
while (true) {
|
|
151
|
+
console.log(allowAll ? "Enter comma-separated values or 'all' for all:" : "Enter comma-separated values:");
|
|
152
|
+
const answer = await ask("> ");
|
|
153
|
+
if (allowAll && answer.toLowerCase() === "all")
|
|
154
|
+
return options.map(o => o.value);
|
|
155
|
+
const tokens = answer
|
|
156
|
+
.split(",")
|
|
157
|
+
.map(n => n.trim())
|
|
158
|
+
.filter(v => v.length > 0);
|
|
159
|
+
if (tokens.length === 0) {
|
|
160
|
+
console.log(red("No values entered. Please try again."));
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const invalid = tokens.filter(v => !validValues.has(v));
|
|
164
|
+
if (invalid.length > 0) {
|
|
165
|
+
console.log(red(`Invalid value(s): ${invalid.join(", ")}. Valid options are: ${[...validValues].join(", ")}.`));
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
// preserveOrder=true keeps duplicates and caller's order (e.g., "1,3,2" stays [1,3,2]).
|
|
169
|
+
return preserveOrder ? tokens : [...new Set(tokens)];
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const askYesNo = async (prompt) => {
|
|
173
|
+
console.log(`\n${prompt}`);
|
|
174
|
+
// eslint-disable-next-line no-constant-condition
|
|
175
|
+
while (true) {
|
|
176
|
+
const answer = (await ask("(y/n): ")).toLowerCase();
|
|
177
|
+
if (answer === "y" || answer === "yes")
|
|
178
|
+
return true;
|
|
179
|
+
if (answer === "n" || answer === "no")
|
|
180
|
+
return false;
|
|
181
|
+
console.log(red(`Invalid input "${answer}". Please enter y or n.`));
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
const pickValidAddresses = async (prompt) => {
|
|
185
|
+
console.log(`\n${prompt}`);
|
|
186
|
+
// eslint-disable-next-line no-constant-condition
|
|
187
|
+
while (true) {
|
|
188
|
+
const answer = await ask("> ");
|
|
189
|
+
const tokens = answer
|
|
190
|
+
.split(",")
|
|
191
|
+
.map(a => a.trim())
|
|
192
|
+
.filter(a => a.length > 0);
|
|
193
|
+
if (tokens.length === 0) {
|
|
194
|
+
console.log(red("No addresses entered. Please try again."));
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const invalid = tokens.filter(a => !hardhat_1.ethers.utils.isAddress(a));
|
|
198
|
+
if (invalid.length > 0) {
|
|
199
|
+
console.log(red(`Invalid address(es): ${invalid.join(", ")}. Please enter valid Ethereum addresses.`));
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
return [...new Set(tokens)];
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
// Pure parser: validates a user-entered uint256 string. Returns normalized
|
|
206
|
+
// decimal string or null if invalid / negative. Extracted so unit tests can
|
|
207
|
+
// cover every reject path without driving readline.
|
|
208
|
+
const parseUint256String = (raw) => {
|
|
209
|
+
try {
|
|
210
|
+
const bn = hardhat_1.ethers.BigNumber.from(raw);
|
|
211
|
+
if (bn.lt(0))
|
|
212
|
+
return null;
|
|
213
|
+
return bn.toString();
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
exports.parseUint256String = parseUint256String;
|
|
220
|
+
const askUint256 = async (prompt) => {
|
|
221
|
+
console.log(`\n${prompt}`);
|
|
222
|
+
// eslint-disable-next-line no-constant-condition
|
|
223
|
+
while (true) {
|
|
224
|
+
const answer = await ask("> ");
|
|
225
|
+
const parsed = (0, exports.parseUint256String)(answer);
|
|
226
|
+
if (parsed !== null)
|
|
227
|
+
return parsed;
|
|
228
|
+
console.log(red(`Invalid uint256 "${answer}". Please enter a non-negative integer.`));
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
// ─── On-chain query helpers ──────────────────────────────────────────────────
|
|
232
|
+
const VTOKEN_ABI = ["function symbol() view returns (string)"];
|
|
233
|
+
const fetchSymbol = async (vToken) => {
|
|
234
|
+
try {
|
|
235
|
+
const contract = new hardhat_1.ethers.Contract(vToken, VTOKEN_ABI, hardhat_1.ethers.provider);
|
|
236
|
+
return await retry(() => contract.symbol());
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return `MARKET_${vToken.slice(2, 8).toUpperCase()}`;
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
const fetchAllMarkets = async (comptroller, isIsolatedPool) => {
|
|
243
|
+
const abi = isIsolatedPool ? IL_COMPTROLLER_QUERY_ABI : BSC_COMPTROLLER_QUERY_ABI;
|
|
244
|
+
const contract = new hardhat_1.ethers.Contract(comptroller, abi, hardhat_1.ethers.provider);
|
|
245
|
+
const allMarkets = await retry(() => contract.getAllMarkets());
|
|
246
|
+
console.log(`Found ${allMarkets.length} market(s) on comptroller.`);
|
|
247
|
+
return allMarkets;
|
|
248
|
+
};
|
|
249
|
+
const filterListedAndFetchSymbols = async (comptroller, isIsolatedPool, markets, logUnlisted) => {
|
|
250
|
+
const abi = isIsolatedPool ? IL_COMPTROLLER_QUERY_ABI : BSC_COMPTROLLER_QUERY_ABI;
|
|
251
|
+
const contract = new hardhat_1.ethers.Contract(comptroller, abi, hardhat_1.ethers.provider);
|
|
252
|
+
console.log(`Checking ${markets.length} market(s)...`);
|
|
253
|
+
const results = await Promise.all(markets.map(async (market) => {
|
|
254
|
+
const { isListed } = await retry(() => contract.markets(market));
|
|
255
|
+
if (isListed) {
|
|
256
|
+
const symbol = await fetchSymbol(market);
|
|
257
|
+
return { market, isListed: true, symbol };
|
|
258
|
+
}
|
|
259
|
+
return { market, isListed: false, symbol: "" };
|
|
260
|
+
}));
|
|
261
|
+
const addresses = [];
|
|
262
|
+
const symbols = new Map();
|
|
263
|
+
for (const { market, isListed, symbol } of results) {
|
|
264
|
+
if (isListed) {
|
|
265
|
+
addresses.push(market);
|
|
266
|
+
symbols.set(market, symbol);
|
|
267
|
+
}
|
|
268
|
+
else if (logUnlisted) {
|
|
269
|
+
console.log(yellow(` Skipping unlisted market: ${market}`));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
console.log(green(` Found ${addresses.length} listed market(s)`));
|
|
273
|
+
return { addresses, symbols };
|
|
274
|
+
};
|
|
275
|
+
const fetchCurrentCF = async (comptroller, vToken, isIsolatedPool, poolId) => {
|
|
276
|
+
if (isIsolatedPool) {
|
|
277
|
+
const contract = new hardhat_1.ethers.Contract(comptroller, IL_COMPTROLLER_QUERY_ABI, hardhat_1.ethers.provider);
|
|
278
|
+
const data = await retry(() => contract.markets(vToken));
|
|
279
|
+
return { cf: data.collateralFactorMantissa, lt: data.liquidationThresholdMantissa };
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
const contract = new hardhat_1.ethers.Contract(comptroller, BSC_COMPTROLLER_QUERY_ABI, hardhat_1.ethers.provider);
|
|
283
|
+
const actualPoolId = poolId !== undefined ? poolId : (await retry(() => contract.corePoolId())).toNumber();
|
|
284
|
+
const data = await retry(() => contract.poolMarkets(actualPoolId, vToken));
|
|
285
|
+
return { cf: data.collateralFactorMantissa, lt: data.liquidationThresholdMantissa };
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
const fetchCurrentCaps = async (comptroller, vToken, isIsolatedPool) => {
|
|
289
|
+
const abi = isIsolatedPool ? IL_COMPTROLLER_QUERY_ABI : BSC_COMPTROLLER_QUERY_ABI;
|
|
290
|
+
const contract = new hardhat_1.ethers.Contract(comptroller, abi, hardhat_1.ethers.provider);
|
|
291
|
+
const [borrowCap, supplyCap] = await Promise.all([
|
|
292
|
+
retry(() => contract.borrowCaps(vToken)),
|
|
293
|
+
retry(() => contract.supplyCaps(vToken)),
|
|
294
|
+
]);
|
|
295
|
+
return { borrowCap, supplyCap };
|
|
296
|
+
};
|
|
297
|
+
const buildMarketsFromChain = async (comptroller, isIsolatedPool) => {
|
|
298
|
+
console.log("\nQuerying comptroller for all markets...");
|
|
299
|
+
const allMarkets = await fetchAllMarkets(comptroller, isIsolatedPool);
|
|
300
|
+
const { addresses, symbols } = await filterListedAndFetchSymbols(comptroller, isIsolatedPool, allMarkets, false);
|
|
301
|
+
const markets = {};
|
|
302
|
+
const collisions = [];
|
|
303
|
+
for (const addr of addresses) {
|
|
304
|
+
const sym = symbols.get(addr);
|
|
305
|
+
if (markets[sym]) {
|
|
306
|
+
collisions.push(`${sym}: ${markets[sym]} and ${addr}`);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
markets[sym] = addr;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (collisions.length > 0) {
|
|
313
|
+
console.error(red("Symbol collision(s) detected:"));
|
|
314
|
+
collisions.forEach(c => console.error(red(` ${c}`)));
|
|
315
|
+
console.error(red("Cannot build markets map with duplicate symbol keys."));
|
|
316
|
+
rl.close();
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}
|
|
319
|
+
return markets;
|
|
320
|
+
};
|
|
321
|
+
// ─── Network helpers ─────────────────────────────────────────────────────────
|
|
322
|
+
const getEBrakeAddress = async (networkName) => {
|
|
323
|
+
// Try to load from hardhat-deploy artifact
|
|
324
|
+
const artifactPath = path.resolve(__dirname, "..", "deployments", networkName, "EBrake.json");
|
|
325
|
+
if (fs.existsSync(artifactPath)) {
|
|
326
|
+
try {
|
|
327
|
+
const artifact = JSON.parse(fs.readFileSync(artifactPath, "utf-8"));
|
|
328
|
+
if (hardhat_1.ethers.utils.isAddress(artifact.address)) {
|
|
329
|
+
console.log(green(`EBrake address from deployment artifact: ${artifact.address}`));
|
|
330
|
+
return artifact.address;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
// Fall through to manual prompt
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
console.log(`No EBrake deployment artifact found for network "${networkName}".`);
|
|
338
|
+
// eslint-disable-next-line no-constant-condition
|
|
339
|
+
while (true) {
|
|
340
|
+
const entered = await ask("Enter EBrake proxy address manually: ");
|
|
341
|
+
if (hardhat_1.ethers.utils.isAddress(entered))
|
|
342
|
+
return entered;
|
|
343
|
+
console.log(red(`Invalid address "${entered}". Please enter a valid Ethereum address.`));
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
exports.getEBrakeAddress = getEBrakeAddress;
|
|
347
|
+
const DEFAULT_SAFE_ADDRESS = "0xCCa5a587eBDBe80f23c8610F2e53B03158e62948";
|
|
348
|
+
const askSafeAddress = async () => {
|
|
349
|
+
console.log("\nEnter the Gnosis Safe address that will sign this transaction.");
|
|
350
|
+
console.log(`Press Enter to use the default: ${DEFAULT_SAFE_ADDRESS}`);
|
|
351
|
+
// eslint-disable-next-line no-constant-condition
|
|
352
|
+
while (true) {
|
|
353
|
+
const answer = (await ask("> ")).trim();
|
|
354
|
+
if (answer === "")
|
|
355
|
+
return DEFAULT_SAFE_ADDRESS;
|
|
356
|
+
if (hardhat_1.ethers.utils.isAddress(answer))
|
|
357
|
+
return answer;
|
|
358
|
+
console.log(red(`Invalid address "${answer}". Please enter a valid Ethereum address.`));
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
// ─── Market selection helper ─────────────────────────────────────────────────
|
|
362
|
+
const pickSymbols = async (entries) => {
|
|
363
|
+
const validSymbols = new Set(entries.map(([s]) => s));
|
|
364
|
+
console.log("\nEnter comma-separated names or 'all':");
|
|
365
|
+
// eslint-disable-next-line no-constant-condition
|
|
366
|
+
while (true) {
|
|
367
|
+
const answer = await ask("> ");
|
|
368
|
+
if (answer.trim().toLowerCase() === "all")
|
|
369
|
+
return entries;
|
|
370
|
+
const tokens = answer
|
|
371
|
+
.split(",")
|
|
372
|
+
.map(n => n.trim())
|
|
373
|
+
.filter(v => v.length > 0);
|
|
374
|
+
if (tokens.length === 0) {
|
|
375
|
+
console.log(red("No names entered. Please try again."));
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
const invalid = tokens.filter(t => !validSymbols.has(t));
|
|
379
|
+
if (invalid.length > 0) {
|
|
380
|
+
console.log(red(`Unknown symbol(s): ${invalid.join(", ")}. Valid symbols: ${[...validSymbols].join(", ")}.`));
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
const dedup = [...new Set(tokens)];
|
|
384
|
+
return dedup.map(sym => entries.find(([s]) => s === sym));
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
// Pure parser: parses a pool-id list answer ("0,6" or "all") against the set
|
|
388
|
+
// of available pools. Returns the picked pool IDs (deduped, order preserved)
|
|
389
|
+
// or null if the answer is empty / contains unknown IDs. Extracted for unit
|
|
390
|
+
// testing without driving readline.
|
|
391
|
+
const parsePoolIdList = (answer, available) => {
|
|
392
|
+
const validSet = new Set(available);
|
|
393
|
+
const t = answer.trim().toLowerCase();
|
|
394
|
+
if (t === "all")
|
|
395
|
+
return available;
|
|
396
|
+
const tokens = answer
|
|
397
|
+
.split(",")
|
|
398
|
+
.map(s => s.trim())
|
|
399
|
+
.filter(v => v.length > 0);
|
|
400
|
+
if (tokens.length === 0)
|
|
401
|
+
return null;
|
|
402
|
+
const parsed = tokens.map(Number);
|
|
403
|
+
if (parsed.some(n => !Number.isInteger(n) || !validSet.has(n)))
|
|
404
|
+
return null;
|
|
405
|
+
return [...new Set(parsed)];
|
|
406
|
+
};
|
|
407
|
+
exports.parsePoolIdList = parsePoolIdList;
|
|
408
|
+
// Asks operator to pick which pool IDs a market should act on. Called per
|
|
409
|
+
// market after market selection — prevents blind fan-out to every pool the
|
|
410
|
+
// market happens to be listed in when only a subset is intended.
|
|
411
|
+
const pickPoolIds = async (sym, available) => {
|
|
412
|
+
console.log(cyan(`\n${sym} is listed in pool(s): [${available.join(", ")}]`));
|
|
413
|
+
console.log("Enter comma-separated pool IDs or 'all':");
|
|
414
|
+
// eslint-disable-next-line no-constant-condition
|
|
415
|
+
while (true) {
|
|
416
|
+
const answer = await ask("> ");
|
|
417
|
+
const picked = (0, exports.parsePoolIdList)(answer, available);
|
|
418
|
+
if (picked !== null)
|
|
419
|
+
return picked;
|
|
420
|
+
console.log(red(`Invalid pool ID entry "${answer}". Valid: ${available.join(", ")}.`));
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
const selectMarkets = async (ctx) => {
|
|
424
|
+
const { comptrollerAddress: comptroller, isIsolatedPool } = ctx;
|
|
425
|
+
const marketMode = await pickOne("How to load markets?", [
|
|
426
|
+
"Enter addresses manually in CLI",
|
|
427
|
+
"Select by name (auto-fetch all listed markets on first use, cached for this run)",
|
|
428
|
+
]);
|
|
429
|
+
let marketAddresses = [];
|
|
430
|
+
let symbols = new Map();
|
|
431
|
+
if (marketMode.startsWith("Enter")) {
|
|
432
|
+
const entered = await pickValidAddresses("Enter market addresses (comma-separated):");
|
|
433
|
+
const result = await filterListedAndFetchSymbols(comptroller, isIsolatedPool, entered, true);
|
|
434
|
+
marketAddresses = result.addresses;
|
|
435
|
+
symbols = result.symbols;
|
|
436
|
+
console.log(`\nUsing ${marketAddresses.length} address(es):`);
|
|
437
|
+
marketAddresses.forEach(addr => console.log(` ${symbols.get(addr)} (${addr})`));
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
let networkMarkets = ctx.marketsCache;
|
|
441
|
+
if (!networkMarkets) {
|
|
442
|
+
networkMarkets = await buildMarketsFromChain(comptroller, isIsolatedPool);
|
|
443
|
+
ctx.marketsCache = networkMarkets;
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
console.log(`Reusing ${Object.keys(networkMarkets).length} market(s) fetched earlier in this run.`);
|
|
447
|
+
}
|
|
448
|
+
const entries = Object.entries(networkMarkets);
|
|
449
|
+
console.log(`Available markets: ${entries.map(([sym]) => sym).join(", ")}`);
|
|
450
|
+
const selected = await pickSymbols(entries);
|
|
451
|
+
marketAddresses = selected.map(([, addr]) => addr);
|
|
452
|
+
symbols = new Map(selected.map(([sym, addr]) => [addr, sym]));
|
|
453
|
+
console.log(`\nSelected ${marketAddresses.length} market(s):`);
|
|
454
|
+
selected.forEach(([sym, addr]) => console.log(` ${sym} (${addr})`));
|
|
455
|
+
}
|
|
456
|
+
if (marketAddresses.length === 0) {
|
|
457
|
+
console.error(red("\nNo listed markets found. Exiting."));
|
|
458
|
+
rl.close();
|
|
459
|
+
process.exit(1);
|
|
460
|
+
}
|
|
461
|
+
return { marketAddresses, symbols };
|
|
462
|
+
};
|
|
463
|
+
// A market can be listed in multiple pools (core + e-mode) with different
|
|
464
|
+
// settings, so we walk every (poolId, market) pair via poolMarkets().isListed.
|
|
465
|
+
// markets().marketPoolId only surfaces one pool per market and would miss the others.
|
|
466
|
+
const enumerateBscPools = async (comptroller) => {
|
|
467
|
+
const contract = new hardhat_1.ethers.Contract(comptroller, BSC_COMPTROLLER_QUERY_ABI, hardhat_1.ethers.provider);
|
|
468
|
+
const [allMarkets, lastPoolIdBn] = await Promise.all([
|
|
469
|
+
retry(() => contract.getAllMarkets()),
|
|
470
|
+
retry(() => contract.lastPoolId()),
|
|
471
|
+
]);
|
|
472
|
+
const lastPoolId = lastPoolIdBn.toNumber();
|
|
473
|
+
console.log(`Scanning ${allMarkets.length} market(s) across ${lastPoolId + 1} pool(s)...`);
|
|
474
|
+
// Fetch each symbol once — a market in multiple pools reuses the same symbol.
|
|
475
|
+
const symbolEntries = await Promise.all(allMarkets.map(async (m) => [m, await fetchSymbol(m)]));
|
|
476
|
+
const symbolMap = new Map(symbolEntries);
|
|
477
|
+
const pools = [];
|
|
478
|
+
for (let poolId = 0; poolId <= lastPoolId; poolId++) {
|
|
479
|
+
const listed = await Promise.all(allMarkets.map(async (market) => {
|
|
480
|
+
const data = await retry(() => contract.poolMarkets(poolId, market));
|
|
481
|
+
return data.isListed ? market : null;
|
|
482
|
+
}));
|
|
483
|
+
const addresses = listed.filter((m) => m !== null);
|
|
484
|
+
if (addresses.length === 0)
|
|
485
|
+
continue;
|
|
486
|
+
const poolSymbols = new Map();
|
|
487
|
+
addresses.forEach(a => poolSymbols.set(a, symbolMap.get(a) ?? a));
|
|
488
|
+
pools.push({ poolId, addresses, symbols: poolSymbols });
|
|
489
|
+
}
|
|
490
|
+
return pools;
|
|
491
|
+
};
|
|
492
|
+
// Lists pools for context, then asks the operator to pick markets from the
|
|
493
|
+
// cross-pool union. Each picked market is mapped to the pool IDs it's listed in,
|
|
494
|
+
// so commandsForStep can fan out one tx per (market, pool) pair. Pools without
|
|
495
|
+
// the market are skipped automatically — they never appear in poolIdsByMarket.
|
|
496
|
+
const pickBscMarketsFanOut = async (ctx) => {
|
|
497
|
+
let pools = ctx.bscPoolsCache;
|
|
498
|
+
if (!pools) {
|
|
499
|
+
pools = await enumerateBscPools(ctx.comptrollerAddress);
|
|
500
|
+
ctx.bscPoolsCache = pools;
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
console.log(`Reusing ${pools.length} pool(s) fetched earlier in this run.`);
|
|
504
|
+
}
|
|
505
|
+
if (pools.length === 0) {
|
|
506
|
+
console.error(red("\nNo listed markets found on any pool. Exiting."));
|
|
507
|
+
rl.close();
|
|
508
|
+
process.exit(1);
|
|
509
|
+
}
|
|
510
|
+
// Build {market → [poolIds]} and {symbol ↔ address} across the union.
|
|
511
|
+
const poolsByMarket = new Map();
|
|
512
|
+
const addressToSymbol = new Map();
|
|
513
|
+
for (const p of pools) {
|
|
514
|
+
for (const addr of p.addresses) {
|
|
515
|
+
const list = poolsByMarket.get(addr) ?? [];
|
|
516
|
+
list.push(p.poolId);
|
|
517
|
+
poolsByMarket.set(addr, list);
|
|
518
|
+
addressToSymbol.set(addr, p.symbols.get(addr) ?? addr);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
const lastPoolId = Math.max(...pools.map(p => p.poolId));
|
|
522
|
+
const allSymbols = Array.from(addressToSymbol.values()).join(", ");
|
|
523
|
+
console.log(cyan(`\n${pools.length} pools available (lastPoolId = ${lastPoolId})`));
|
|
524
|
+
console.log(`Available markets: ${allSymbols}`);
|
|
525
|
+
const entries = Array.from(addressToSymbol, ([addr, sym]) => [sym, addr]);
|
|
526
|
+
const selected = await pickSymbols(entries);
|
|
527
|
+
const marketAddresses = selected.map(([, addr]) => addr);
|
|
528
|
+
const symbols = new Map(selected.map(([sym, addr]) => [addr, sym]));
|
|
529
|
+
// Per-market pool picker: operator chooses which pool IDs to act on.
|
|
530
|
+
// Markets listed in a single pool skip the prompt (nothing to pick).
|
|
531
|
+
console.log(cyan("\n--- Pool selection per market ---"));
|
|
532
|
+
const poolIdsByMarket = new Map();
|
|
533
|
+
for (const addr of marketAddresses) {
|
|
534
|
+
const sym = symbols.get(addr) ?? addr;
|
|
535
|
+
const available = poolsByMarket.get(addr) ?? [];
|
|
536
|
+
if (available.length <= 1) {
|
|
537
|
+
poolIdsByMarket.set(addr, available);
|
|
538
|
+
if (available.length === 1) {
|
|
539
|
+
console.log(`${sym} → pool [${available[0]}] (only pool, no selection needed)`);
|
|
540
|
+
}
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
const picked = await pickPoolIds(sym, available);
|
|
544
|
+
poolIdsByMarket.set(addr, picked);
|
|
545
|
+
}
|
|
546
|
+
console.log(cyan("\nFan-out plan:"));
|
|
547
|
+
let totalTxs = 0;
|
|
548
|
+
for (const addr of marketAddresses) {
|
|
549
|
+
const sym = symbols.get(addr) ?? addr;
|
|
550
|
+
const ids = poolIdsByMarket.get(addr) ?? [];
|
|
551
|
+
totalTxs += ids.length;
|
|
552
|
+
console.log(` ${sym} → pool(s) [${ids.join(", ")}]`);
|
|
553
|
+
}
|
|
554
|
+
// Guard against fat-finger 'all' on a market with many pools — explicit
|
|
555
|
+
// confirm before the batch is generated.
|
|
556
|
+
const proceed = await askYesNo(`About to fan out ${totalTxs} tx(s) across the selection. Proceed?`);
|
|
557
|
+
if (!proceed) {
|
|
558
|
+
console.log(yellow("Aborted by operator. No batch generated."));
|
|
559
|
+
rl.close();
|
|
560
|
+
process.exit(0);
|
|
561
|
+
}
|
|
562
|
+
return { marketAddresses, symbols, poolIdsByMarket };
|
|
563
|
+
};
|
|
564
|
+
const parsePerMarketValuesJson = (raw, markets, symbols) => {
|
|
565
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
566
|
+
return { kind: "notObject" };
|
|
567
|
+
const symbolToAddress = new Map();
|
|
568
|
+
symbols.forEach((sym, addr) => symbolToAddress.set(sym, addr));
|
|
569
|
+
const selectionSet = new Set(markets);
|
|
570
|
+
const values = new Map();
|
|
571
|
+
const ignored = [];
|
|
572
|
+
for (const [key, rawValue] of Object.entries(raw)) {
|
|
573
|
+
const addr = hardhat_1.ethers.utils.isAddress(key) ? key : symbolToAddress.get(key);
|
|
574
|
+
if (!addr || !selectionSet.has(addr)) {
|
|
575
|
+
ignored.push(key);
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
try {
|
|
579
|
+
const bn = hardhat_1.ethers.BigNumber.from(rawValue);
|
|
580
|
+
if (bn.lt(0))
|
|
581
|
+
throw new Error("neg");
|
|
582
|
+
values.set(addr, bn.toString());
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
return { kind: "invalidValue", key, rawValue };
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
const missingSymbols = markets.filter(m => !values.has(m)).map(m => symbols.get(m) || m);
|
|
589
|
+
if (missingSymbols.length > 0)
|
|
590
|
+
return { kind: "missing", missingSymbols, ignored };
|
|
591
|
+
return { kind: "ok", values, ignored };
|
|
592
|
+
};
|
|
593
|
+
exports.parsePerMarketValuesJson = parsePerMarketValuesJson;
|
|
594
|
+
// Collect per-market values (CF or cap) via one of three modes: uniform, per-market CLI, or file.
|
|
595
|
+
const gatherPerMarketValues = async (markets, symbols, cfg) => {
|
|
596
|
+
const mode = await pickOne(`Apply ${cfg.kind}:`, [
|
|
597
|
+
`Single value to ALL selected markets (e.g. 0 to block)`,
|
|
598
|
+
`Per-market values via CLI prompts`,
|
|
599
|
+
`Load values from a JSON file`,
|
|
600
|
+
]);
|
|
601
|
+
const result = new Map();
|
|
602
|
+
const fetchAllCurrent = async () => {
|
|
603
|
+
const entries = await Promise.all(markets.map(async (m) => [m, await cfg.fetchCurrent(m)]));
|
|
604
|
+
return new Map(entries);
|
|
605
|
+
};
|
|
606
|
+
if (mode.startsWith("Single")) {
|
|
607
|
+
const value = await askUint256(`Enter new ${cfg.kind} for all selected markets:`);
|
|
608
|
+
markets.forEach(m => result.set(m, value));
|
|
609
|
+
}
|
|
610
|
+
else if (mode.startsWith("Per-market")) {
|
|
611
|
+
const current = await fetchAllCurrent();
|
|
612
|
+
for (const m of markets) {
|
|
613
|
+
const sym = symbols.get(m) || m;
|
|
614
|
+
const value = await askUint256(` ${sym} [current: ${current.get(m)}]:`);
|
|
615
|
+
result.set(m, value);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
const defaultPath = path.resolve(OUTPUT_DIR, cfg.defaultFilename);
|
|
620
|
+
// Print expected file shape before asking for the path so operators can
|
|
621
|
+
// prepare the file rather than discovering the format by trial and error.
|
|
622
|
+
console.log(cyan(`\nExpected ${cfg.kind} file format:`));
|
|
623
|
+
console.log(` JSON object mapping symbol (or address) → ${cfg.kind} value.`);
|
|
624
|
+
console.log(` Example:`);
|
|
625
|
+
console.log(` {`);
|
|
626
|
+
console.log(` "vAAVE": "0",`);
|
|
627
|
+
console.log(` "vBTCB": "500000000000000000"`);
|
|
628
|
+
console.log(` }`);
|
|
629
|
+
console.log(` Address keys are accepted too. Values must be non-negative integers.`);
|
|
630
|
+
console.log(` If the file doesn't exist, a template pre-filled with current on-chain`);
|
|
631
|
+
console.log(` values will be offered — edit it, save, re-run.`);
|
|
632
|
+
// Re-prompt on any validation failure so a typo in the path or the file's
|
|
633
|
+
// contents doesn't force the operator to restart the whole flow. The last
|
|
634
|
+
// entered path is remembered so the post-template-gen retry lands on the
|
|
635
|
+
// same file the operator just edited.
|
|
636
|
+
let stickyDefault = defaultPath;
|
|
637
|
+
// eslint-disable-next-line no-constant-condition
|
|
638
|
+
while (true) {
|
|
639
|
+
console.log(`\nEnter path to ${cfg.kind} values file (press enter for default: ${stickyDefault})`);
|
|
640
|
+
const entered = await ask("> ");
|
|
641
|
+
const filePath = entered.length > 0 ? entered : stickyDefault;
|
|
642
|
+
stickyDefault = filePath;
|
|
643
|
+
// Local helper: writes a fresh template for the current selection, backs
|
|
644
|
+
// up any prior content to .bak, then pauses for operator edit+resume.
|
|
645
|
+
const regenerateTemplateAndPause = async (reason) => {
|
|
646
|
+
if (reason === "missing" && fs.existsSync(filePath)) {
|
|
647
|
+
const backup = `${filePath}.bak`;
|
|
648
|
+
fs.copyFileSync(filePath, backup);
|
|
649
|
+
console.log(yellow(`\nExisting file backed up to ${backup} before regeneration.`));
|
|
650
|
+
}
|
|
651
|
+
console.log(`\nGenerating template with current on-chain ${cfg.kind} values...`);
|
|
652
|
+
const current = await fetchAllCurrent();
|
|
653
|
+
const template = {};
|
|
654
|
+
for (const m of markets) {
|
|
655
|
+
const sym = symbols.get(m) || m;
|
|
656
|
+
template[sym] = current.get(m) ?? "0";
|
|
657
|
+
}
|
|
658
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
659
|
+
fs.writeFileSync(filePath, JSON.stringify(template, null, 2) + "\n");
|
|
660
|
+
console.log(green(`Template written to ${filePath}`));
|
|
661
|
+
console.log(`Edit the values (they currently match on-chain ${cfg.kind}), save, then press Enter to continue.`);
|
|
662
|
+
console.log("Type 'q' + Enter to quit.");
|
|
663
|
+
const resume = (await ask("> ")).trim().toLowerCase();
|
|
664
|
+
if (resume === "q") {
|
|
665
|
+
rl.close();
|
|
666
|
+
process.exit(0);
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
if (!fs.existsSync(filePath)) {
|
|
670
|
+
const confirm = await askYesNo(`${filePath} not found. Generate template there?`);
|
|
671
|
+
if (!confirm)
|
|
672
|
+
continue;
|
|
673
|
+
await regenerateTemplateAndPause("new");
|
|
674
|
+
// Fall through: loop restarts. stickyDefault = filePath, so Enter keeps the same file.
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
let content;
|
|
678
|
+
try {
|
|
679
|
+
content = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
682
|
+
console.error(red(`Failed to parse ${filePath}: ${error.message}`));
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
const parsed = (0, exports.parsePerMarketValuesJson)(content, markets, symbols);
|
|
686
|
+
if (parsed.kind === "notObject") {
|
|
687
|
+
console.error(red(`${filePath} must contain a JSON object mapping symbol-or-address → value.`));
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
if (parsed.kind === "invalidValue") {
|
|
691
|
+
console.error(red(`Invalid ${cfg.kind} value for "${parsed.key}": ${String(parsed.rawValue)}`));
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
if (parsed.kind === "missing") {
|
|
695
|
+
parsed.ignored.forEach(k => console.log(yellow(` Ignoring "${k}" — not in current selection`)));
|
|
696
|
+
console.log(yellow(`\nFile stale — missing entries for: ${parsed.missingSymbols.join(", ")}.`));
|
|
697
|
+
console.log(yellow(`Regenerating template for current selection. Existing file will be backed up.`));
|
|
698
|
+
await regenerateTemplateAndPause("missing");
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
parsed.ignored.forEach(k => console.log(yellow(` Ignoring "${k}" — not in current selection`)));
|
|
702
|
+
result.clear();
|
|
703
|
+
parsed.values.forEach((v, k) => result.set(k, v));
|
|
704
|
+
const current = await fetchAllCurrent();
|
|
705
|
+
console.log("\nWill apply:");
|
|
706
|
+
for (const m of markets) {
|
|
707
|
+
const sym = symbols.get(m) || m;
|
|
708
|
+
console.log(` ${sym} new=${result.get(m)} [current: ${current.get(m)}]`);
|
|
709
|
+
}
|
|
710
|
+
break;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
return result;
|
|
714
|
+
};
|
|
715
|
+
const parsePerPoolValuesJson = (raw, markets, symbols, poolIdsByMarket) => {
|
|
716
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
717
|
+
return { kind: "notObject" };
|
|
718
|
+
const symbolToAddress = new Map();
|
|
719
|
+
symbols.forEach((sym, addr) => symbolToAddress.set(sym, addr));
|
|
720
|
+
const selectionSet = new Set(markets);
|
|
721
|
+
const values = new Map();
|
|
722
|
+
const setVal = (addr, pid, val) => {
|
|
723
|
+
let inner = values.get(addr);
|
|
724
|
+
if (!inner) {
|
|
725
|
+
inner = new Map();
|
|
726
|
+
values.set(addr, inner);
|
|
727
|
+
}
|
|
728
|
+
inner.set(pid, val);
|
|
729
|
+
};
|
|
730
|
+
const ignoredKeys = [];
|
|
731
|
+
const ignoredPools = [];
|
|
732
|
+
for (const [key, rawInner] of Object.entries(raw)) {
|
|
733
|
+
const addr = hardhat_1.ethers.utils.isAddress(key) ? key : symbolToAddress.get(key);
|
|
734
|
+
if (!addr || !selectionSet.has(addr)) {
|
|
735
|
+
ignoredKeys.push(key);
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
if (typeof rawInner !== "object" || rawInner === null || Array.isArray(rawInner)) {
|
|
739
|
+
return { kind: "innerNotObject", key };
|
|
740
|
+
}
|
|
741
|
+
const allowedPools = new Set(poolIdsByMarket.get(addr) ?? []);
|
|
742
|
+
for (const [pidKey, rawValue] of Object.entries(rawInner)) {
|
|
743
|
+
const pid = Number(pidKey);
|
|
744
|
+
if (!Number.isInteger(pid) || !allowedPools.has(pid)) {
|
|
745
|
+
ignoredPools.push({ key, poolIdKey: pidKey });
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
try {
|
|
749
|
+
const bn = hardhat_1.ethers.BigNumber.from(rawValue);
|
|
750
|
+
if (bn.lt(0))
|
|
751
|
+
throw new Error("neg");
|
|
752
|
+
setVal(addr, pid, bn.toString());
|
|
753
|
+
}
|
|
754
|
+
catch {
|
|
755
|
+
return { kind: "invalidValue", key, poolId: pid, rawValue };
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
const missing = [];
|
|
760
|
+
for (const m of markets) {
|
|
761
|
+
for (const pid of poolIdsByMarket.get(m) ?? []) {
|
|
762
|
+
if (!values.get(m)?.has(pid))
|
|
763
|
+
missing.push(`${symbols.get(m) || m} pool=${pid}`);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (missing.length > 0)
|
|
767
|
+
return { kind: "missing", missing, ignoredKeys, ignoredPools };
|
|
768
|
+
return { kind: "ok", values, ignoredKeys, ignoredPools };
|
|
769
|
+
};
|
|
770
|
+
exports.parsePerPoolValuesJson = parsePerPoolValuesJson;
|
|
771
|
+
// Collect per (market, poolId) values for ops that act on a market-in-pool tuple
|
|
772
|
+
// (decrease_cf_pool). Without this, a market listed in N pools would reuse the
|
|
773
|
+
// same value across all pools, making the per-pool op indistinguishable from the
|
|
774
|
+
// all-pools variant.
|
|
775
|
+
const gatherPerPoolValues = async (markets, symbols, poolIdsByMarket, cfg) => {
|
|
776
|
+
const pairs = [];
|
|
777
|
+
for (const m of markets) {
|
|
778
|
+
for (const pid of poolIdsByMarket.get(m) ?? [])
|
|
779
|
+
pairs.push({ market: m, poolId: pid });
|
|
780
|
+
}
|
|
781
|
+
const mode = await pickOne(`Apply ${cfg.kind}:`, [
|
|
782
|
+
`Single value to ALL selected (market, pool) pairs (e.g. 0 to block)`,
|
|
783
|
+
`Per-pair values via CLI prompts`,
|
|
784
|
+
`Load values from a JSON file`,
|
|
785
|
+
]);
|
|
786
|
+
const result = new Map();
|
|
787
|
+
const setVal = (market, poolId, value) => {
|
|
788
|
+
let inner = result.get(market);
|
|
789
|
+
if (!inner) {
|
|
790
|
+
inner = new Map();
|
|
791
|
+
result.set(market, inner);
|
|
792
|
+
}
|
|
793
|
+
inner.set(poolId, value);
|
|
794
|
+
};
|
|
795
|
+
const fetchAllCurrent = async () => {
|
|
796
|
+
const out = new Map();
|
|
797
|
+
await Promise.all(pairs.map(async ({ market, poolId }) => {
|
|
798
|
+
const v = await cfg.fetchCurrent(market, poolId);
|
|
799
|
+
let inner = out.get(market);
|
|
800
|
+
if (!inner) {
|
|
801
|
+
inner = new Map();
|
|
802
|
+
out.set(market, inner);
|
|
803
|
+
}
|
|
804
|
+
inner.set(poolId, v);
|
|
805
|
+
}));
|
|
806
|
+
return out;
|
|
807
|
+
};
|
|
808
|
+
if (mode.startsWith("Single")) {
|
|
809
|
+
const value = await askUint256(`Enter new ${cfg.kind} for all selected (market, pool) pairs:`);
|
|
810
|
+
for (const { market, poolId } of pairs)
|
|
811
|
+
setVal(market, poolId, value);
|
|
812
|
+
}
|
|
813
|
+
else if (mode.startsWith("Per-pair")) {
|
|
814
|
+
const current = await fetchAllCurrent();
|
|
815
|
+
for (const { market, poolId } of pairs) {
|
|
816
|
+
const sym = symbols.get(market) || market;
|
|
817
|
+
const cur = current.get(market)?.get(poolId) ?? "(unknown)";
|
|
818
|
+
const value = await askUint256(` ${sym} pool=${poolId} [current: ${cur}]:`);
|
|
819
|
+
setVal(market, poolId, value);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
else {
|
|
823
|
+
const defaultPath = path.resolve(OUTPUT_DIR, cfg.defaultFilename);
|
|
824
|
+
// Print expected file shape before asking for the path so operators can
|
|
825
|
+
// prepare the file rather than discovering the format by trial and error.
|
|
826
|
+
console.log(cyan(`\nExpected ${cfg.kind} file format (per-pool):`));
|
|
827
|
+
console.log(` JSON object: symbol (or address) → { poolId → ${cfg.kind} value }.`);
|
|
828
|
+
console.log(` Example (vAAVE listed in pools 0 and 6):`);
|
|
829
|
+
console.log(` {`);
|
|
830
|
+
console.log(` "vAAVE": {`);
|
|
831
|
+
console.log(` "0": "0",`);
|
|
832
|
+
console.log(` "6": "700000000000000000"`);
|
|
833
|
+
console.log(` }`);
|
|
834
|
+
console.log(` }`);
|
|
835
|
+
console.log(` Only poolIds the market is actually listed in are accepted.`);
|
|
836
|
+
console.log(` Values must be non-negative integers.`);
|
|
837
|
+
console.log(` If the file doesn't exist, a template pre-filled with current on-chain`);
|
|
838
|
+
console.log(` values will be offered — edit it, save, re-run.`);
|
|
839
|
+
// The last entered path is remembered so the post-template-gen retry lands
|
|
840
|
+
// on the same file the operator just edited.
|
|
841
|
+
let stickyDefault = defaultPath;
|
|
842
|
+
// eslint-disable-next-line no-constant-condition
|
|
843
|
+
while (true) {
|
|
844
|
+
console.log(`\nEnter path to ${cfg.kind} values file (press enter for default: ${stickyDefault})`);
|
|
845
|
+
const entered = await ask("> ");
|
|
846
|
+
const filePath = entered.length > 0 ? entered : stickyDefault;
|
|
847
|
+
stickyDefault = filePath;
|
|
848
|
+
// Local helper: writes a fresh template for the current (market,pool)
|
|
849
|
+
// pairs, backs up prior content to .bak on overwrite, then pauses for
|
|
850
|
+
// operator edit+resume.
|
|
851
|
+
const regenerateTemplateAndPause = async (reason) => {
|
|
852
|
+
if (reason === "missing" && fs.existsSync(filePath)) {
|
|
853
|
+
const backup = `${filePath}.bak`;
|
|
854
|
+
fs.copyFileSync(filePath, backup);
|
|
855
|
+
console.log(yellow(`\nExisting file backed up to ${backup} before regeneration.`));
|
|
856
|
+
}
|
|
857
|
+
console.log(`\nGenerating template with current on-chain ${cfg.kind} values...`);
|
|
858
|
+
const current = await fetchAllCurrent();
|
|
859
|
+
const template = {};
|
|
860
|
+
for (const { market, poolId } of pairs) {
|
|
861
|
+
const sym = symbols.get(market) || market;
|
|
862
|
+
template[sym] ?? (template[sym] = {});
|
|
863
|
+
template[sym][String(poolId)] = current.get(market)?.get(poolId) ?? "0";
|
|
864
|
+
}
|
|
865
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
866
|
+
fs.writeFileSync(filePath, JSON.stringify(template, null, 2) + "\n");
|
|
867
|
+
console.log(green(`Template written to ${filePath}`));
|
|
868
|
+
console.log(`Edit the values (they currently match on-chain ${cfg.kind}), save, then press Enter to continue.`);
|
|
869
|
+
console.log("Type 'q' + Enter to quit.");
|
|
870
|
+
const resume = (await ask("> ")).trim().toLowerCase();
|
|
871
|
+
if (resume === "q") {
|
|
872
|
+
rl.close();
|
|
873
|
+
process.exit(0);
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
if (!fs.existsSync(filePath)) {
|
|
877
|
+
const confirm = await askYesNo(`${filePath} not found. Generate template there?`);
|
|
878
|
+
if (!confirm)
|
|
879
|
+
continue;
|
|
880
|
+
await regenerateTemplateAndPause("new");
|
|
881
|
+
// Fall through: loop restarts, file now exists, same path prompt accepts Enter for default.
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
let content;
|
|
885
|
+
try {
|
|
886
|
+
content = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
887
|
+
}
|
|
888
|
+
catch (error) {
|
|
889
|
+
console.error(red(`Failed to parse ${filePath}: ${error.message}`));
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
const parsed = (0, exports.parsePerPoolValuesJson)(content, markets, symbols, poolIdsByMarket);
|
|
893
|
+
const logIgnored = (p) => {
|
|
894
|
+
if ("ignoredKeys" in p) {
|
|
895
|
+
p.ignoredKeys.forEach(k => console.log(yellow(` Ignoring "${k}" — not in current selection`)));
|
|
896
|
+
p.ignoredPools.forEach(({ key, poolIdKey }) => console.log(yellow(` Ignoring "${key}" poolId "${poolIdKey}" — market not listed in that pool`)));
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
if (parsed.kind === "notObject") {
|
|
900
|
+
console.error(red(`${filePath} must contain a JSON object mapping symbol-or-address → {poolId: value}.`));
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
if (parsed.kind === "innerNotObject") {
|
|
904
|
+
console.error(red(`Entry for "${parsed.key}" must be an object {poolId: value}.`));
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
if (parsed.kind === "invalidValue") {
|
|
908
|
+
console.error(red(`Invalid ${cfg.kind} value for "${parsed.key}" pool ${parsed.poolId}: ${String(parsed.rawValue)}`));
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
if (parsed.kind === "missing") {
|
|
912
|
+
logIgnored(parsed);
|
|
913
|
+
console.log(yellow(`\nFile stale — missing entries for: ${parsed.missing.join(", ")}.`));
|
|
914
|
+
console.log(yellow(`Regenerating template for current selection. Existing file will be backed up.`));
|
|
915
|
+
await regenerateTemplateAndPause("missing");
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
logIgnored(parsed);
|
|
919
|
+
result.clear();
|
|
920
|
+
parsed.values.forEach((inner, addr) => {
|
|
921
|
+
inner.forEach((v, pid) => setVal(addr, pid, v));
|
|
922
|
+
});
|
|
923
|
+
const current = await fetchAllCurrent();
|
|
924
|
+
console.log("\nWill apply:");
|
|
925
|
+
for (const { market, poolId } of pairs) {
|
|
926
|
+
const sym = symbols.get(market) || market;
|
|
927
|
+
const cur = current.get(market)?.get(poolId) ?? "(unknown)";
|
|
928
|
+
const val = result.get(market)?.get(poolId) ?? "";
|
|
929
|
+
console.log(` ${sym} pool=${poolId} new=${val} [current: ${cur}]`);
|
|
930
|
+
}
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
return result;
|
|
935
|
+
};
|
|
936
|
+
// Collect the per-operation inputs (markets, actions, CFs, caps, etc.) for a single step.
|
|
937
|
+
const gatherStep = async (operation, ctx) => {
|
|
938
|
+
const { comptrollerAddress, isIsolatedPool } = ctx;
|
|
939
|
+
let marketAddresses = [];
|
|
940
|
+
let symbols = new Map();
|
|
941
|
+
let pauseActions;
|
|
942
|
+
let newCFs;
|
|
943
|
+
let newCFsByPool;
|
|
944
|
+
let poolIdsByMarket;
|
|
945
|
+
let newCaps;
|
|
946
|
+
let revokeAccounts;
|
|
947
|
+
if (operation === "pause_flash_loan") {
|
|
948
|
+
const confirmed = await askYesNo("Pause all flash loans on the core pool?");
|
|
949
|
+
if (!confirmed) {
|
|
950
|
+
console.log("Aborted.");
|
|
951
|
+
rl.close();
|
|
952
|
+
process.exit(0);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
else if (operation === "revoke_flash_loan") {
|
|
956
|
+
revokeAccounts = await pickValidAddresses("Enter account address(es) to revoke flash loan access from (comma-separated):");
|
|
957
|
+
console.log(`\nRevoking flash loan access from ${revokeAccounts.length} account(s).`);
|
|
958
|
+
}
|
|
959
|
+
else {
|
|
960
|
+
if (operation === "decrease_cf_pool" || operation === "disable_pool_borrow") {
|
|
961
|
+
console.log(cyan("\n--- Pool & Market Selection ---"));
|
|
962
|
+
const picked = await pickBscMarketsFanOut(ctx);
|
|
963
|
+
poolIdsByMarket = picked.poolIdsByMarket;
|
|
964
|
+
marketAddresses = picked.marketAddresses;
|
|
965
|
+
symbols = picked.symbols;
|
|
966
|
+
}
|
|
967
|
+
else {
|
|
968
|
+
console.log(cyan("\n--- Market Selection ---"));
|
|
969
|
+
const result = await selectMarkets(ctx);
|
|
970
|
+
marketAddresses = result.marketAddresses;
|
|
971
|
+
symbols = result.symbols;
|
|
972
|
+
}
|
|
973
|
+
if (operation === "pause_actions") {
|
|
974
|
+
const actionOptions = Object.entries(exports.ALLOWED_PAUSE_ACTIONS).map(([name, value]) => ({
|
|
975
|
+
name,
|
|
976
|
+
value: String(value),
|
|
977
|
+
}));
|
|
978
|
+
const selected = await pickMultiple("Select actions to pause (only MINT/REDEEM/BORROW/TRANSFER are allowed by EBrake):", actionOptions);
|
|
979
|
+
pauseActions = selected.map(Number);
|
|
980
|
+
if (pauseActions.length === 0) {
|
|
981
|
+
console.error(red("No actions selected. Exiting."));
|
|
982
|
+
rl.close();
|
|
983
|
+
process.exit(1);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
else if (operation === "decrease_cf") {
|
|
987
|
+
newCFs = await gatherPerMarketValues(marketAddresses, symbols, {
|
|
988
|
+
kind: "CF",
|
|
989
|
+
defaultFilename: "cf_values.json",
|
|
990
|
+
fetchCurrent: async (m) => {
|
|
991
|
+
try {
|
|
992
|
+
const { cf } = await fetchCurrentCF(comptrollerAddress, m, isIsolatedPool);
|
|
993
|
+
return cf.toString();
|
|
994
|
+
}
|
|
995
|
+
catch {
|
|
996
|
+
return "(not listed)";
|
|
997
|
+
}
|
|
998
|
+
},
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
else if (operation === "decrease_cf_pool") {
|
|
1002
|
+
if (!poolIdsByMarket)
|
|
1003
|
+
throw new Error("poolIdsByMarket missing for decrease_cf_pool");
|
|
1004
|
+
newCFsByPool = await gatherPerPoolValues(marketAddresses, symbols, poolIdsByMarket, {
|
|
1005
|
+
kind: "CF",
|
|
1006
|
+
defaultFilename: "cf_values_per_pool.json",
|
|
1007
|
+
fetchCurrent: async (m, pid) => {
|
|
1008
|
+
try {
|
|
1009
|
+
const { cf } = await fetchCurrentCF(comptrollerAddress, m, isIsolatedPool, pid);
|
|
1010
|
+
return cf.toString();
|
|
1011
|
+
}
|
|
1012
|
+
catch {
|
|
1013
|
+
return "(not listed)";
|
|
1014
|
+
}
|
|
1015
|
+
},
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
else if (operation === "set_borrow_caps") {
|
|
1019
|
+
newCaps = await gatherPerMarketValues(marketAddresses, symbols, {
|
|
1020
|
+
kind: "borrow cap",
|
|
1021
|
+
defaultFilename: "borrow_caps.json",
|
|
1022
|
+
fetchCurrent: async (m) => {
|
|
1023
|
+
const { borrowCap } = await fetchCurrentCaps(comptrollerAddress, m, isIsolatedPool);
|
|
1024
|
+
return borrowCap.toString();
|
|
1025
|
+
},
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
else if (operation === "set_supply_caps") {
|
|
1029
|
+
newCaps = await gatherPerMarketValues(marketAddresses, symbols, {
|
|
1030
|
+
kind: "supply cap",
|
|
1031
|
+
defaultFilename: "supply_caps.json",
|
|
1032
|
+
fetchCurrent: async (m) => {
|
|
1033
|
+
const { supplyCap } = await fetchCurrentCaps(comptrollerAddress, m, isIsolatedPool);
|
|
1034
|
+
return supplyCap.toString();
|
|
1035
|
+
},
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return {
|
|
1040
|
+
operation,
|
|
1041
|
+
marketAddresses,
|
|
1042
|
+
symbols,
|
|
1043
|
+
pauseActions,
|
|
1044
|
+
newCFs,
|
|
1045
|
+
newCFsByPool,
|
|
1046
|
+
poolIdsByMarket,
|
|
1047
|
+
newCaps,
|
|
1048
|
+
revokeAccounts,
|
|
1049
|
+
};
|
|
1050
|
+
};
|
|
1051
|
+
const gatherInput = async () => {
|
|
1052
|
+
const networkName = hardhat_1.network.name;
|
|
1053
|
+
const chainId = hardhat_1.network.config.chainId;
|
|
1054
|
+
if (!chainId) {
|
|
1055
|
+
console.error(red(`No chainId configured for network "${networkName}".`));
|
|
1056
|
+
rl.close();
|
|
1057
|
+
process.exit(1);
|
|
1058
|
+
}
|
|
1059
|
+
console.log(cyan(`=== Safe EBrake JSON Generator (${networkName}, chain ${chainId}) ===\n`));
|
|
1060
|
+
// 1. Resolve EBrake address + probe
|
|
1061
|
+
const eBrakeAddress = await (0, exports.getEBrakeAddress)(networkName);
|
|
1062
|
+
const eBrake = new hardhat_1.ethers.Contract(eBrakeAddress, EBRAKE_ABI, hardhat_1.ethers.provider);
|
|
1063
|
+
const [isIsolatedPool, comptrollerAddress] = await Promise.all([
|
|
1064
|
+
retry(() => eBrake.IS_ISOLATED_POOL()),
|
|
1065
|
+
retry(() => eBrake.COMPTROLLER()),
|
|
1066
|
+
]);
|
|
1067
|
+
console.log(` IS_ISOLATED_POOL: ${isIsolatedPool}`);
|
|
1068
|
+
console.log(` COMPTROLLER: ${comptrollerAddress}\n`);
|
|
1069
|
+
// 2. Operation menu — BSC-only options shown only when !isIsolatedPool
|
|
1070
|
+
const operationChoices = [
|
|
1071
|
+
{ label: "Pause actions on markets (pauseActions)", value: "pause_actions" },
|
|
1072
|
+
{ label: "Decrease collateral factor — all pools (decreaseCF)", value: "decrease_cf" },
|
|
1073
|
+
{ label: "Decrease borrow caps (setMarketBorrowCaps)", value: "set_borrow_caps" },
|
|
1074
|
+
{ label: "Decrease supply caps (setMarketSupplyCaps)", value: "set_supply_caps" },
|
|
1075
|
+
];
|
|
1076
|
+
if (!isIsolatedPool) {
|
|
1077
|
+
operationChoices.push({ label: "[BSC] Decrease CF — specific pool (decreaseCF with poolId)", value: "decrease_cf_pool" }, { label: "[BSC] Pause flash loans (pauseFlashLoan)", value: "pause_flash_loan" }, { label: "[BSC] Revoke flash loan access (revokeFlashLoanAccess)", value: "revoke_flash_loan" }, { label: "[BSC] Disable pool borrow (disablePoolBorrow)", value: "disable_pool_borrow" });
|
|
1078
|
+
}
|
|
1079
|
+
// 3. Upfront multi-select: pick ALL operations to batch.
|
|
1080
|
+
// Order-preserving + duplicates allowed (same op twice with different markets is valid).
|
|
1081
|
+
const choiceOptions = operationChoices.map((c, i) => ({ name: c.label, value: String(i + 1) }));
|
|
1082
|
+
const selectedIndices = await pickMultiple("Select operations (comma-separated, e.g. 1,2,5):", choiceOptions, {
|
|
1083
|
+
allowAll: false,
|
|
1084
|
+
preserveOrder: true,
|
|
1085
|
+
});
|
|
1086
|
+
const selectedOps = selectedIndices.map(i => operationChoices[parseInt(i, 10) - 1].value);
|
|
1087
|
+
// 4. Iterate selected operations, collecting per-step inputs.
|
|
1088
|
+
const ctx = { comptrollerAddress, isIsolatedPool };
|
|
1089
|
+
const steps = [];
|
|
1090
|
+
for (let i = 0; i < selectedOps.length; i++) {
|
|
1091
|
+
const op = selectedOps[i];
|
|
1092
|
+
console.log(cyan(`\n─── Step ${i + 1}/${selectedOps.length}: ${op} ───`));
|
|
1093
|
+
steps.push(await gatherStep(op, ctx));
|
|
1094
|
+
}
|
|
1095
|
+
// 6. Ask for the Safe (once) using the first step's role hint.
|
|
1096
|
+
const safeAddress = await askSafeAddress();
|
|
1097
|
+
const blockNumber = await hardhat_1.ethers.provider.getBlockNumber();
|
|
1098
|
+
return {
|
|
1099
|
+
eBrakeAddress,
|
|
1100
|
+
comptrollerAddress,
|
|
1101
|
+
isIsolatedPool,
|
|
1102
|
+
network: networkName,
|
|
1103
|
+
chainId,
|
|
1104
|
+
steps,
|
|
1105
|
+
safeAddress,
|
|
1106
|
+
blockNumber,
|
|
1107
|
+
};
|
|
1108
|
+
};
|
|
1109
|
+
exports.gatherInput = gatherInput;
|
|
1110
|
+
// ─── Phase 2: Generate Commands ──────────────────────────────────────────────
|
|
1111
|
+
const commandsForStep = (step) => {
|
|
1112
|
+
const { operation, marketAddresses, symbols, pauseActions, newCFs, newCFsByPool, poolIdsByMarket, newCaps, revokeAccounts, } = step;
|
|
1113
|
+
const commands = [];
|
|
1114
|
+
switch (operation) {
|
|
1115
|
+
case "pause_actions": {
|
|
1116
|
+
if (!pauseActions || pauseActions.length === 0)
|
|
1117
|
+
break;
|
|
1118
|
+
const actionNames = pauseActions
|
|
1119
|
+
.map(a => Object.entries(exports.ALLOWED_PAUSE_ACTIONS).find(([, v]) => v === a)?.[0] || String(a))
|
|
1120
|
+
.join(", ");
|
|
1121
|
+
console.log(`\nAdding pauseActions for ${marketAddresses.length} market(s): [${actionNames}]`);
|
|
1122
|
+
commands.push({
|
|
1123
|
+
signature: "pauseActions(address[],uint8[])",
|
|
1124
|
+
params: [marketAddresses, pauseActions],
|
|
1125
|
+
});
|
|
1126
|
+
break;
|
|
1127
|
+
}
|
|
1128
|
+
case "decrease_cf": {
|
|
1129
|
+
if (!newCFs || newCFs.size === 0)
|
|
1130
|
+
break;
|
|
1131
|
+
for (const [vToken, cf] of newCFs) {
|
|
1132
|
+
const symbol = symbols.get(vToken) || vToken;
|
|
1133
|
+
console.log(` ${symbol} → decreaseCF(${vToken}, ${cf})`);
|
|
1134
|
+
commands.push({
|
|
1135
|
+
signature: "decreaseCF(address,uint256)",
|
|
1136
|
+
params: [vToken, cf],
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
break;
|
|
1140
|
+
}
|
|
1141
|
+
case "decrease_cf_pool": {
|
|
1142
|
+
if (!newCFsByPool || newCFsByPool.size === 0)
|
|
1143
|
+
break;
|
|
1144
|
+
for (const [vToken, byPool] of newCFsByPool) {
|
|
1145
|
+
const symbol = symbols.get(vToken) || vToken;
|
|
1146
|
+
for (const [pid, cf] of byPool) {
|
|
1147
|
+
console.log(` ${symbol} → decreaseCF(${vToken}, poolId=${pid}, ${cf})`);
|
|
1148
|
+
commands.push({
|
|
1149
|
+
signature: "decreaseCF(address,uint96,uint256)",
|
|
1150
|
+
params: [vToken, pid, cf],
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
break;
|
|
1155
|
+
}
|
|
1156
|
+
case "set_borrow_caps": {
|
|
1157
|
+
if (!newCaps || newCaps.size === 0)
|
|
1158
|
+
break;
|
|
1159
|
+
const markets = [...newCaps.keys()];
|
|
1160
|
+
const caps = markets.map(m => newCaps.get(m));
|
|
1161
|
+
console.log(`\nAdding setMarketBorrowCaps for ${markets.length} market(s)`);
|
|
1162
|
+
commands.push({
|
|
1163
|
+
signature: "setMarketBorrowCaps(address[],uint256[])",
|
|
1164
|
+
params: [markets, caps],
|
|
1165
|
+
});
|
|
1166
|
+
break;
|
|
1167
|
+
}
|
|
1168
|
+
case "set_supply_caps": {
|
|
1169
|
+
if (!newCaps || newCaps.size === 0)
|
|
1170
|
+
break;
|
|
1171
|
+
const markets = [...newCaps.keys()];
|
|
1172
|
+
const caps = markets.map(m => newCaps.get(m));
|
|
1173
|
+
console.log(`\nAdding setMarketSupplyCaps for ${markets.length} market(s)`);
|
|
1174
|
+
commands.push({
|
|
1175
|
+
signature: "setMarketSupplyCaps(address[],uint256[])",
|
|
1176
|
+
params: [markets, caps],
|
|
1177
|
+
});
|
|
1178
|
+
break;
|
|
1179
|
+
}
|
|
1180
|
+
case "pause_flash_loan": {
|
|
1181
|
+
console.log("\nAdding pauseFlashLoan()");
|
|
1182
|
+
commands.push({ signature: "pauseFlashLoan()", params: [] });
|
|
1183
|
+
break;
|
|
1184
|
+
}
|
|
1185
|
+
case "revoke_flash_loan": {
|
|
1186
|
+
if (!revokeAccounts || revokeAccounts.length === 0)
|
|
1187
|
+
break;
|
|
1188
|
+
for (const account of revokeAccounts) {
|
|
1189
|
+
console.log(` → revokeFlashLoanAccess(${account})`);
|
|
1190
|
+
commands.push({
|
|
1191
|
+
signature: "revokeFlashLoanAccess(address)",
|
|
1192
|
+
params: [account],
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
break;
|
|
1196
|
+
}
|
|
1197
|
+
case "disable_pool_borrow": {
|
|
1198
|
+
if (!poolIdsByMarket)
|
|
1199
|
+
break;
|
|
1200
|
+
for (const vToken of marketAddresses) {
|
|
1201
|
+
const symbol = symbols.get(vToken) || vToken;
|
|
1202
|
+
const poolIds = poolIdsByMarket.get(vToken) ?? [];
|
|
1203
|
+
for (const pid of poolIds) {
|
|
1204
|
+
console.log(` ${symbol} → disablePoolBorrow(poolId=${pid}, ${vToken})`);
|
|
1205
|
+
commands.push({
|
|
1206
|
+
signature: "disablePoolBorrow(uint96,address)",
|
|
1207
|
+
params: [pid, vToken],
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
break;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
return commands;
|
|
1215
|
+
};
|
|
1216
|
+
const generateCommands = (batch) => batch.steps.flatMap((step, i) => {
|
|
1217
|
+
console.log(`\n[Step ${i + 1}/${batch.steps.length}] ${step.operation}`);
|
|
1218
|
+
return commandsForStep(step);
|
|
1219
|
+
});
|
|
1220
|
+
exports.generateCommands = generateCommands;
|
|
1221
|
+
// ─── Phase 3: Export JSON ─────────────────────────────────────────────────────
|
|
1222
|
+
const exportJson = async (commands, batch, safeAddress) => {
|
|
1223
|
+
if (commands.length === 0) {
|
|
1224
|
+
console.log("No commands generated. Skipping.");
|
|
1225
|
+
return null;
|
|
1226
|
+
}
|
|
1227
|
+
const txBuilderFile = path.resolve(OUTPUT_DIR, `safeEBrakeTxBuilder.json`);
|
|
1228
|
+
const metadataFile = path.resolve(OUTPUT_DIR, `safeEBrakeTxMetadata.json`);
|
|
1229
|
+
// Direct calldata encoding — no governance VIP wrapper
|
|
1230
|
+
const iface = new hardhat_1.ethers.utils.Interface(EBRAKE_ABI);
|
|
1231
|
+
const txData = commands.map(cmd => ({
|
|
1232
|
+
to: batch.eBrakeAddress,
|
|
1233
|
+
value: "0",
|
|
1234
|
+
data: iface.encodeFunctionData(cmd.signature, cmd.params),
|
|
1235
|
+
}));
|
|
1236
|
+
const outputJson = (0, exports.buildSafeBatch)(safeAddress, txData, batch.chainId, batch.blockNumber);
|
|
1237
|
+
// Merge symbols across all steps (a given address may appear in multiple steps;
|
|
1238
|
+
// last write wins, but they should be identical since symbols come from chain).
|
|
1239
|
+
const symbolsRecord = {};
|
|
1240
|
+
for (const step of batch.steps) {
|
|
1241
|
+
step.symbols.forEach((sym, addr) => {
|
|
1242
|
+
symbolsRecord[addr] = sym;
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
const metadata = {
|
|
1246
|
+
eBrakeAddress: batch.eBrakeAddress,
|
|
1247
|
+
comptrollerAddress: batch.comptrollerAddress,
|
|
1248
|
+
network: batch.network,
|
|
1249
|
+
operations: batch.steps.map(s => s.operation),
|
|
1250
|
+
blockNumber: batch.blockNumber,
|
|
1251
|
+
createdAt: new Date().toISOString(),
|
|
1252
|
+
symbols: symbolsRecord,
|
|
1253
|
+
};
|
|
1254
|
+
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
1255
|
+
fs.writeFileSync(txBuilderFile, JSON.stringify(outputJson, null, 2));
|
|
1256
|
+
fs.writeFileSync(metadataFile, JSON.stringify(metadata, null, 2));
|
|
1257
|
+
return { txBuilderFile, metadataFile, txCount: commands.length, safeAddress };
|
|
1258
|
+
};
|
|
1259
|
+
exports.exportJson = exportJson;
|
|
1260
|
+
// ─── Phase 4: Orchestration ──────────────────────────────────────────────────
|
|
1261
|
+
const printBatchSummary = (batch, txCount) => {
|
|
1262
|
+
console.log(cyan("\n─── Batch summary ───"));
|
|
1263
|
+
batch.steps.forEach((step, i) => {
|
|
1264
|
+
console.log(` [${i + 1}/${batch.steps.length}] ${step.operation}`);
|
|
1265
|
+
});
|
|
1266
|
+
console.log(` Total transactions: ${txCount}`);
|
|
1267
|
+
console.log(` Network: ${batch.network} (chain ${batch.chainId})`);
|
|
1268
|
+
console.log(` EBrake: ${batch.eBrakeAddress}`);
|
|
1269
|
+
console.log(` Safe: ${batch.safeAddress}`);
|
|
1270
|
+
};
|
|
1271
|
+
const orchestrate = async (batch) => {
|
|
1272
|
+
console.log(cyan("\n--- Generating commands ---"));
|
|
1273
|
+
const commands = (0, exports.generateCommands)(batch);
|
|
1274
|
+
printBatchSummary(batch, commands.length);
|
|
1275
|
+
return (0, exports.exportJson)(commands, batch, batch.safeAddress);
|
|
1276
|
+
};
|
|
1277
|
+
exports.orchestrate = orchestrate;
|
|
1278
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
1279
|
+
const printResult = (r, networkName) => {
|
|
1280
|
+
console.log(cyan("\n=== Output ==="));
|
|
1281
|
+
console.log(` Safe TX Builder JSON: ${r.txBuilderFile}`);
|
|
1282
|
+
console.log(` Metadata: ${r.metadataFile}`);
|
|
1283
|
+
console.log(` Transactions: ${r.txCount}`);
|
|
1284
|
+
console.log(` Safe address: ${r.safeAddress}`);
|
|
1285
|
+
console.log(` Simulate: ${bold(`npx hardhat test scripts/simulateSafeEBrakeTx.ts --fork ${networkName}`)}`);
|
|
1286
|
+
};
|
|
1287
|
+
const main = async () => {
|
|
1288
|
+
const input = await (0, exports.gatherInput)();
|
|
1289
|
+
console.log(cyan("\n--- Processing ---"));
|
|
1290
|
+
const result = await (0, exports.orchestrate)(input);
|
|
1291
|
+
if (result) {
|
|
1292
|
+
printResult(result, input.network);
|
|
1293
|
+
}
|
|
1294
|
+
rl.close();
|
|
1295
|
+
};
|
|
1296
|
+
exports.main = main;
|
|
1297
|
+
if (require.main === module) {
|
|
1298
|
+
(0, exports.main)().catch(err => {
|
|
1299
|
+
console.error(err);
|
|
1300
|
+
process.exit(1);
|
|
1301
|
+
});
|
|
1302
|
+
}
|