Files
smom-dbis-138/scripts/chain138/seed-uni-v2-weth-quote-pairs.js

275 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Create and seed Uniswap V2 pairs on Chain 138: each canonical asset vs WETH9 (ERC-20) in the pair.
*
* Funding the ETH leg (no explicit wrap in this script):
* - Liquidity is added via UniswapV2Router02.addLiquidityETH: you send native ETH as `msg.value`.
* - The router wraps to WETH9 inside the call and mints LP; this script never calls WETH9.deposit().
* - Pair reserves are still WETH + token (AMM math cannot store raw ETH in the pair contract).
*
* Prerequisites:
* - PRIVATE_KEY; deployer holds native ETH (for `value` + gas) + tokens (or MINTER_ROLE to mint).
* - Factory + router: CHAIN138_UNISWAP_V2_EXISTING_* or deployments/chain138/uniswap-v2-native.json
*
* Usage (from smom-dbis-138):
* source scripts/load-env.sh
* npx hardhat run scripts/chain138/seed-uni-v2-weth-quote-pairs.js --network chain138
*
* Tuning:
* CHAIN138_NATIVE_WETH9 — must match routers WETH (default 0xC02a…)
* CHAIN138_UNI_V2_DEFAULT_WETH — native ETH `value` per new pair (default 0.25)
* CHAIN138_UNI_V2_STABLE_PER_WETH — USD face (6-decimal) per 1 ETH for stables (default 2100)
* CHAIN138_UNI_V2_SLIPPAGE_BPS — min amounts for addLiquidityETH (default 50 = 0.5%)
* CHAIN138_UNI_V2_ETH_GAS_BUFFER_WEI — extra wei required on top of `value` (default ~0.03 ETH for gas)
* Per-pair: CHAIN138_UNI_V2_SEED_<key>_WETH and CHAIN138_UNI_V2_SEED_<key>_TOKEN
*/
const fs = require("fs");
const path = require("path");
const hre = require("hardhat");
const WETH9 = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
/** Canonical Chain 138 tokens (see docs/11-references/EXPLORER_TOKEN_LIST_CROSSCHECK.md). */
const WETH_QUOTE_TARGETS = [
{ key: "wethLink", symbol: "LINK", address: "0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03", decimals: 18, kind: "link" },
{ key: "wethCusdt", symbol: "cUSDT", address: "0x93E66202A11B1772E55407B32B44e5Cd8eda7f22", decimals: 6, kind: "stable6" },
{ key: "wethCusdc", symbol: "cUSDC", address: "0xf22258f57794CC8E06237084b353Ab30fFfa640b", decimals: 6, kind: "stable6" },
{ key: "wethWeth10", symbol: "WETH10", address: "0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f", decimals: 18, kind: "weth10" },
{ key: "wethCeurc", symbol: "cEURC", address: "0x8085961F9cF02b4d800A3c6d386D31da4B34266a", decimals: 6, kind: "stable6" },
{ key: "wethCeurt", symbol: "cEURT", address: "0xdf4b71c61E5912712C1Bdd451416B9aC26949d72", decimals: 6, kind: "stable6" },
{ key: "wethCgbpc", symbol: "cGBPC", address: "0x003960f16D9d34F2e98d62723B6721Fb92074aD2", decimals: 6, kind: "stable6" },
{ key: "wethCgbrt", symbol: "cGBPT", address: "0x350f54e4D23795f86A9c03988c7135357CCaD97c", decimals: 6, kind: "stable6" },
{ key: "wethCaudc", symbol: "cAUDC", address: "0xD51482e567c03899eecE3CAe8a058161FD56069D", decimals: 6, kind: "stable6" },
{ key: "wethCjpyc", symbol: "cJPYC", address: "0xEe269e1226a334182aace90056EE4ee5Cc8A6770", decimals: 6, kind: "stable6" },
{ key: "wethCchfc", symbol: "cCHFC", address: "0x873990849DDa5117d7C644f0aF24370797C03885", decimals: 6, kind: "stable6" },
{ key: "wethCcadc", symbol: "cCADC", address: "0x54dBd40cF05e15906A2C21f600937e96787f5679", decimals: 6, kind: "stable6" },
{ key: "wethCxauc", symbol: "cXAUC", address: "0x290E52a8819A4fbD0714E517225429aA2B70EC6b", decimals: 6, kind: "xau6" },
{ key: "wethCxaut", symbol: "cXAUT", address: "0x94e408E26c6FD8F4ee00b54dF19082FDA07dC96E", decimals: 6, kind: "xau6" },
{ key: "wethUsdt", symbol: "USDT", address: "0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1", decimals: 6, kind: "stable6" },
{ key: "wethUsdc", symbol: "USDC", address: "0x71D6687F38b93CCad569Fa6352c876eea967201b", decimals: 6, kind: "stable6" },
];
const erc20Abi = [
"function transfer(address to,uint256 amount) returns (bool)",
"function approve(address spender,uint256 amount) returns (bool)",
"function balanceOf(address account) view returns (uint256)",
];
const routerAbi = [
"function addLiquidityETH(address token,uint amountTokenDesired,uint amountTokenMin,uint amountETHMin,address to,uint deadline) payable returns (uint amountToken, uint amountETH, uint liquidity)",
];
const mintAbi = ["function mint(address to,uint256 amount)"];
function defaultWethFloat() {
return parseFloat(process.env.CHAIN138_UNI_V2_DEFAULT_WETH || "0.25");
}
function stablePerWethUsd() {
return parseFloat(process.env.CHAIN138_UNI_V2_STABLE_PER_WETH || "2100");
}
async function ensureMinted(tokenAddr, signer, need) {
const c = await hre.ethers.getContractAt(erc20Abi, tokenAddr, signer);
const bal = await c.balanceOf(signer.address);
if (bal >= need) return;
const short = need - bal;
const m = await hre.ethers.getContractAt(mintAbi, tokenAddr, signer);
await (await m.mint(signer.address, short)).wait();
}
async function ensureBalance(tokenAddr, signer, need, meta) {
const c = await hre.ethers.getContractAt(erc20Abi, tokenAddr, signer);
const bal = await c.balanceOf(signer.address);
if (bal >= need) return;
try {
await ensureMinted(tokenAddr, signer, need);
} catch (e) {
throw new Error(
`${meta.symbol}: need ${hre.ethers.formatUnits(need - bal, meta.decimals)} more; mint failed: ${e.message}`
);
}
}
/** Ensure wallet has enough native ETH for addLiquidityETH `value` plus a gas buffer (this script does not wrap). */
async function ensureNativeEth(signer, valueWei) {
const buffer = BigInt(process.env.CHAIN138_UNI_V2_ETH_GAS_BUFFER_WEI || "30000000000000000");
const need = valueWei + buffer;
const bal = await signer.provider.getBalance(signer.address);
if (bal < need) {
throw new Error(
`Native ETH: need at least ${hre.ethers.formatEther(need)} (${hre.ethers.formatEther(valueWei)} for pool + buffer); have ${hre.ethers.formatEther(bal)}`
);
}
}
function computeAmounts(meta) {
const envW = process.env[`CHAIN138_UNI_V2_SEED_${meta.key}_WETH`];
const envT = process.env[`CHAIN138_UNI_V2_SEED_${meta.key}_TOKEN`];
const ethWei =
envW !== undefined && envW !== ""
? hre.ethers.parseEther(String(envW))
: hre.ethers.parseEther(String(defaultWethFloat()));
const wethEth = parseFloat(hre.ethers.formatEther(ethWei));
let tokenStr;
if (envT !== undefined && envT !== "") {
tokenStr = String(envT);
} else {
switch (meta.kind) {
case "stable6":
tokenStr = String(Math.max(1, Math.round(stablePerWethUsd() * wethEth)));
break;
case "link":
tokenStr = String(Math.max(1, Math.round(100 * wethEth)));
break;
case "weth10":
tokenStr = hre.ethers.formatEther(ethWei);
break;
case "xau6":
tokenStr = String(Math.max(1, Math.round(1 * Math.max(wethEth, 0.01))));
break;
default:
tokenStr = "1";
}
}
const tokenAmt = hre.ethers.parseUnits(tokenStr, meta.decimals);
return { ethWei, tokenAmt };
}
function minAmounts(amount, bps) {
return (amount * (10000n - bps)) / 10000n;
}
async function main() {
const signers = await hre.ethers.getSigners();
if (!signers?.length) {
throw new Error("chain138: no signer — set PRIVATE_KEY (32-byte hex) for network chain138 in env");
}
const deployer = signers[0];
const gasPrice = BigInt(process.env.CHAIN138_NATIVE_V2_GAS_PRICE || (await hre.ethers.provider.send("eth_gasPrice", [])));
const txOverrides = {
type: 0,
gasPrice,
gasLimit: BigInt(process.env.CHAIN138_NATIVE_V2_LIQUIDITY_GAS_LIMIT || "3000000"),
};
const deploymentPath = path.resolve(__dirname, "../../deployments/chain138/uniswap-v2-native.json");
let factory = (process.env.CHAIN138_UNISWAP_V2_EXISTING_FACTORY || "").trim();
let routerAddr = (process.env.CHAIN138_UNISWAP_V2_EXISTING_ROUTER || "").trim();
if (fs.existsSync(deploymentPath)) {
const j = JSON.parse(fs.readFileSync(deploymentPath, "utf8"));
if (!factory) factory = j.factory;
if (!routerAddr) routerAddr = j.router;
}
if (!factory) {
throw new Error("Set CHAIN138_UNISWAP_V2_EXISTING_FACTORY or add deployments/chain138/uniswap-v2-native.json");
}
if (!routerAddr) {
throw new Error("Set CHAIN138_UNISWAP_V2_EXISTING_ROUTER or add router to deployments/chain138/uniswap-v2-native.json");
}
const Factory = await hre.ethers.getContractFactory(
"contracts/vendor/uniswap-v2-core/UniswapV2Factory.sol:UniswapV2Factory"
);
const Pair = await hre.ethers.getContractFactory(
"contracts/vendor/uniswap-v2-core/UniswapV2Pair.sol:UniswapV2Pair"
);
const factoryC = Factory.attach(factory);
const wethAddr = (process.env.CHAIN138_NATIVE_WETH9 || process.env.CHAIN138_WETH9_ADDRESS || WETH9).trim();
const router = await hre.ethers.getContractAt(routerAbi, routerAddr, deployer);
console.log(
`[eth] add liquidity via router addLiquidityETH (native ETH \`value\`; no WETH9.deposit() in this script) router=${routerAddr}`
);
console.log(`[weth9] pair reserve token: ${wethAddr} (router wraps ETH inside addLiquidityETH)`);
const slipBps = BigInt(process.env.CHAIN138_UNI_V2_SLIPPAGE_BPS || "50");
const skipKeys = new Set(
(process.env.CHAIN138_UNI_V2_SKIP_KEYS || "")
.split(",")
.map((s) => s.trim())
.filter(Boolean)
);
/** @type {{ key: string, pair: string, status: string }[]} */
const report = [];
for (const meta of WETH_QUOTE_TARGETS) {
if (skipKeys.has(meta.key)) {
report.push({ key: meta.key, pair: "", status: "skip_env" });
console.log(`[skip] ${meta.symbol} (${meta.key}) via CHAIN138_UNI_V2_SKIP_KEYS`);
continue;
}
const tokenAddr = meta.address;
if (tokenAddr.toLowerCase() === wethAddr.toLowerCase()) continue;
const { ethWei, tokenAmt } = computeAmounts(meta);
let pairAddress = await factoryC.getPair(wethAddr, tokenAddr);
if (pairAddress === hre.ethers.ZeroAddress) {
const tx = await factoryC.createPair(wethAddr, tokenAddr, txOverrides);
await tx.wait();
pairAddress = await factoryC.getPair(wethAddr, tokenAddr);
}
const pair = Pair.connect(deployer).attach(pairAddress);
const [r0, r1] = await pair.getReserves();
if (r0 > 0n || r1 > 0n) {
report.push({ key: meta.key, pair: pairAddress, status: "skip_existing_liquidity" });
console.log(`[skip] ${meta.symbol} already funded ${pairAddress}`);
continue;
}
const tokenC = await hre.ethers.getContractAt(erc20Abi, tokenAddr, deployer);
await ensureNativeEth(deployer, ethWei);
await ensureBalance(tokenAddr, deployer, tokenAmt, meta);
const allowance = await tokenC.allowance(deployer.address, routerAddr);
if (allowance < tokenAmt) {
await (await tokenC.approve(routerAddr, hre.ethers.MaxUint256)).wait();
}
const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600);
const tokenMin = minAmounts(tokenAmt, slipBps);
const ethMin = minAmounts(ethWei, slipBps);
console.log(`[eth] addLiquidityETH ${meta.symbol}: value=${hre.ethers.formatEther(ethWei)} ETH, token=${tokenAmt.toString()}`);
const tx = await router.addLiquidityETH(
tokenAddr,
tokenAmt,
tokenMin,
ethMin,
deployer.address,
deadline,
{ ...txOverrides, value: ethWei }
);
await tx.wait();
report.push({ key: meta.key, pair: pairAddress, status: "seeded" });
console.log(`[ok] ${meta.symbol} ${pairAddress}`);
}
const out = {
chainId: 138,
deployer: deployer.address,
factory,
router: routerAddr,
weth9: wethAddr,
results: report,
};
const outPath = path.resolve(__dirname, "../../deployments/chain138/uni-v2-weth-quote-seed.json");
fs.mkdirSync(path.dirname(outPath), { recursive: true });
fs.writeFileSync(outPath, JSON.stringify(out, null, 2) + "\n");
console.log(JSON.stringify(out, null, 2));
}
main().catch((e) => {
console.error(e);
process.exit(1);
});