275 lines
12 KiB
JavaScript
275 lines
12 KiB
JavaScript
/**
|
||
* 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 router’s 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);
|
||
});
|