From 8e8a703fc03794a48878d2f7cb11390c54bf1aa3 Mon Sep 17 00:00:00 2001 From: MonoPX Date: Tue, 25 Nov 2025 00:15:58 +0300 Subject: [PATCH] Update borrow-eth-with-erc20-collateral.js --- .../borrow-eth-with-erc20-collateral.js | 325 ++++++++++-------- 1 file changed, 178 insertions(+), 147 deletions(-) diff --git a/examples-js/ethers-js/borrow-eth-with-erc20-collateral.js b/examples-js/ethers-js/borrow-eth-with-erc20-collateral.js index b56dc22..490fa4f 100644 --- a/examples-js/ethers-js/borrow-eth-with-erc20-collateral.js +++ b/examples-js/ethers-js/borrow-eth-with-erc20-collateral.js @@ -1,155 +1,186 @@ -// Example to supply a supported ERC20 token as collateral and borrow ETH -// YOU MUST HAVE DAI IN YOUR WALLET before you run this script -const ethers = require('ethers'); -const provider = new ethers.providers.JsonRpcProvider('http://localhost:8545'); -const { - cEthAbi, - comptrollerAbi, - priceFeedAbi, - cErcAbi, - erc20Abi, -} = require('../../contracts.json'); - -// Your Ethereum wallet private key -const privateKey = 'b8c1b5c1d81f9475fdf2e334517d29f733bdfa40682207571b12fc1142cbf329'; -const wallet = new ethers.Wallet(privateKey, provider); -const myWalletAddress = wallet.address; - -// Mainnet Contract for cETH -const cEthAddress = '0x4ddc2d193948926d02f9b1fe9e1daa0718270ed5'; -const cEth = new ethers.Contract(cEthAddress, cEthAbi, wallet); - -// Mainnet Contract for Compound's Comptroller -const comptrollerAddress = '0x3d9819210a31b4961b30ef54be2aed79b9c9cd3b'; -const comptroller = new ethers.Contract(comptrollerAddress, comptrollerAbi, wallet); - -// Mainnet Contract for the Open Price Feed -const priceFeedAddress = '0x6d2299c48a8dd07a872fdd0f8233924872ad1071'; -const priceFeed = new ethers.Contract(priceFeedAddress, priceFeedAbi, wallet); - -// Mainnet address of underlying token (like DAI or USDC) -const underlyingAddress = '0x6B175474E89094C44Da98b954EedeAC495271d0F'; // Dai -const underlying = new ethers.Contract(underlyingAddress, erc20Abi, wallet); - -// Mainnet address for a cToken (like cDai, https://compound.finance/docs#networks) -const cTokenAddress = '0x5d3a536e4d6dbd6114cc1ead35777bab948e3643'; // cDai -const cToken = new ethers.Contract(cTokenAddress, cErcAbi, wallet); -const assetName = 'DAI'; // for the log output lines -const underlyingDecimals = 18; // Number of decimals defined in this ERC20 token's contract - -const logBalances = () => { - return new Promise(async (resolve, reject) => { - let myWalletEthBalance = await provider.getBalance(myWalletAddress) / 1e18; - let myWalletCTokenBalance = await cToken.callStatic.balanceOf(myWalletAddress) / 1e8; - let myWalletUnderlyingBalance = await underlying.callStatic.balanceOf(myWalletAddress) / Math.pow(10, underlyingDecimals); - +import { existsSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { BigNumber, ethers } from 'ethers'; + +// --- CONFIGURATION CONSTANTS --- +// Mainnet RPC URL for local testing (e.g., using Anvil or a fork) +const RPC_URL = 'http://localhost:8545'; + +// Contract Addresses (Mainnet) +const CETH_ADDRESS = '0x4ddc2d193948926d02f9b1fe9e1daa0718270ed5'; // cETH +const COMPTROLLER_ADDRESS = '0x3d9819210a31b4961b30ef54be2aed79b9c9cd3b'; // Compound Comptroller +const PRICE_FEED_ADDRESS = '0x6d2299c48a8dd07a872fdd0f8233924872ad1071'; // Open Price Feed +const UNDERLYING_ADDRESS = '0x6B175474E89094C44Da98b954EedeAC495271d0F'; // DAI +const CTOKEN_ADDRESS = '0x5d3a536e4d6dbd6114cc1ead35777bab948e3643'; // cDAI + +// Asset Details +const ASSET_NAME = 'DAI'; +const UNDERLYING_DECIMALS = 18; +const ETH_DECIMALS = 18; +const CTOKEN_DECIMALS = 8; // cTokens typically have 8 decimals + +// Amounts +const AMOUNT_TO_SUPPLY = 15; // 15 DAI to supply +const AMOUNT_TO_BORROW = 0.002; // 0.002 ETH to borrow + +// --- HELPER FUNCTIONS --- + +/** + * Loads the private key from environment variables for security. + * @returns {string} The private key. + */ +function getPrivateKey() { + // SECURITY FIX: Never hardcode private keys. + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error("PRIVATE_KEY environment variable is not set. Please set it securely."); + } + return privateKey; +} + +/** + * Checks for a Compound Failure event in a transaction receipt. + * @param {ethers.providers.TransactionReceipt} receipt + * @param {string} operation + */ +function checkCompoundFailure(receipt, operation) { + const failureEvent = receipt.events?.find(e => e.event === 'Failure'); + if (failureEvent) { + const errorCode = failureEvent.args.error.toString(); + throw new Error( + `Compound ${operation} failed with error code: ${errorCode}.\n` + + `Refer to: https://compound.finance/docs/ctokens#ctoken-error-codes` + ); + } +} + +/** + * Logs current ETH, cToken, and Underlying token balances. + */ +const logBalances = async (walletAddress, provider, cToken, underlying) => { + // Use ethers utilities for safe BigNumber handling + const myWalletEthBalanceBN = await provider.getBalance(walletAddress); + const myWalletCTokenBalanceBN = await cToken.callStatic.balanceOf(walletAddress); + const myWalletUnderlyingBalanceBN = await underlying.callStatic.balanceOf(walletAddress); + + // Format for display + const myWalletEthBalance = ethers.utils.formatEther(myWalletEthBalanceBN); + const myWalletCTokenBalance = ethers.utils.formatUnits(myWalletCTokenBalanceBN, CTOKEN_DECIMALS); + const myWalletUnderlyingBalance = ethers.utils.formatUnits(myWalletUnderlyingBalanceBN, UNDERLYING_DECIMALS); + + console.log("--- Balances ---"); console.log("My Wallet's ETH Balance:", myWalletEthBalance); - console.log(`My Wallet's c${assetName} Balance:`, myWalletCTokenBalance); - console.log(`My Wallet's ${assetName} Balance:`, myWalletUnderlyingBalance); - - resolve(); - }); + console.log(`My Wallet's c${ASSET_NAME} Balance:`, myWalletCTokenBalance); + console.log(`My Wallet's ${ASSET_NAME} Balance:`, myWalletUnderlyingBalance); + console.log("----------------\n"); }; +// --- MAIN LOGIC --- + const main = async () => { - await logBalances(); - - let underlyingAsCollateral = 15; - - // Convert the token amount to a scaled up number, then a string. - underlyingAsCollateral = underlyingAsCollateral * Math.pow(10, underlyingDecimals); - underlyingAsCollateral = underlyingAsCollateral.toString(); - - console.log(`\nApproving ${assetName} to be transferred from your wallet to the c${assetName} contract...\n`); - const approve = await underlying.approve(cTokenAddress, underlyingAsCollateral); - await approve.wait(1); - - console.log(`Supplying ${assetName} to the protocol as collateral (you will get c${assetName} in return)...\n`); - let mint = await cToken.mint(underlyingAsCollateral); - const mintResult = await mint.wait(1); - - let failure = mintResult.events.find(_ => _.event === 'Failure'); - if (failure) { - const errorCode = failure.args.error; - throw new Error( - `See https://compound.finance/docs/ctokens#ctoken-error-codes\n` + - `Code: ${errorCode}\n` - ); - } - - await logBalances(); - - console.log('\nEntering market (via Comptroller contract) for ETH (as collateral)...'); - let markets = [cTokenAddress]; // This is the cToken contract(s) for your collateral - let enterMarkets = await comptroller.enterMarkets(markets); - await enterMarkets.wait(1); - - console.log('Calculating your liquid assets in the protocol...'); - let {1:liquidity} = await comptroller.callStatic.getAccountLiquidity(myWalletAddress); - liquidity = (+liquidity / 1e18).toString(); - - console.log(`Fetching the protocol's ${assetName} collateral factor...`); - let {1:collateralFactor} = await comptroller.callStatic.markets(cTokenAddress); - collateralFactor = (collateralFactor / Math.pow(10, underlyingDecimals)) * 100; // Convert to percent - - console.log(`Fetching ${assetName} price from the price feed...`); - let underlyingPriceInUsd = await priceFeed.callStatic.price(assetName); - underlyingPriceInUsd = underlyingPriceInUsd / 1e6; // Price feed provides price in USD with 6 decimal places - - console.log('Fetching borrow rate per block for ETH borrowing...'); - let borrowRate = await cEth.callStatic.borrowRatePerBlock(); - borrowRate = borrowRate / 1e18; - - console.log(`\nYou have ${liquidity} of LIQUID assets (worth of USD) pooled in the protocol.`); - console.log(`You can borrow up to ${collateralFactor}% of your TOTAL assets supplied to the protocol as ETH.`); - console.log(`1 ${assetName} == ${underlyingPriceInUsd.toFixed(6)} USD`); - console.log(`You can borrow up to ${liquidity} USD worth of assets from the protocol.`); - console.log(`NEVER borrow near the maximum amount because your account will be instantly liquidated.`); - console.log(`\nYour borrowed amount INCREASES (${borrowRate} * borrowed amount) ETH per block.\nThis is based on the current borrow rate.`); - - // Let's try to borrow 0.002 ETH (or another amount far below the borrow limit) - const ethToBorrow = 0.002; - console.log(`\nNow attempting to borrow ${ethToBorrow} ETH...`); - const borrow = await cEth.borrow(ethers.utils.parseEther(ethToBorrow.toString())); - const borrowResult = await borrow.wait(1); - - if (isNaN(borrowResult)) { - console.log(`\nETH borrow successful.\n`); - } else { - throw new Error( - `See https://compound.finance/docs/ctokens#ctoken-error-codes\n` + - `Code: ${borrowResult}\n` - ); - } - - await logBalances(); - - console.log('\nFetching your ETH borrow balance from cETH contract...'); - let balance = await cEth.callStatic.borrowBalanceCurrent(myWalletAddress); - balance = balance / 1e18; // because DAI is a 1e18 scaled token. - console.log(`Borrow balance is ${balance} ETH`); - - console.log(`\nThis part is when you do something with those borrowed assets!\n`); - - console.log(`Now repaying the borrow...`); - - const ethToRepay = ethToBorrow; - const repayBorrow = await cEth.repayBorrow({ - value: ethers.utils.parseEther(ethToRepay.toString()) - }); - const repayBorrowResult = await repayBorrow.wait(1); - - failure = repayBorrowResult.events.find(_ => _.event === 'Failure'); - if (failure) { - const errorCode = failure.args.error; - console.error(`repayBorrow error, code ${errorCode}`); - process.exit(1); - } - - console.log(`\nBorrow repaid.\n`); - await logBalances(); + // Load ABIs (assuming contracts.json structure is correct) + const { cEthAbi, comptrollerAbi, priceFeedAbi, cErcAbi, erc20Abi } = require('../../contracts.json'); + + const provider = new ethers.providers.JsonRpcProvider(RPC_URL); + const privateKey = getPrivateKey(); + const wallet = new ethers.Wallet(privateKey, provider); + const myWalletAddress = wallet.address; + + // Contract Instances + const cEth = new ethers.Contract(CETH_ADDRESS, cEthAbi, wallet); + const comptroller = new ethers.Contract(COMPTROLLER_ADDRESS, comptrollerAbi, wallet); + const priceFeed = new ethers.Contract(PRICE_FEED_ADDRESS, priceFeedAbi, wallet); + const underlying = new ethers.Contract(UNDERLYING_ADDRESS, erc20Abi, wallet); + const cToken = new ethers.Contract(CTOKEN_ADDRESS, cErcAbi, wallet); + + await logBalances(myWalletAddress, provider, cToken, underlying); + + // Convert human-readable amount to BigNumber scaled to 18 decimals (DAI) + const amountToSupplyBN = ethers.utils.parseUnits(AMOUNT_TO_SUPPLY.toString(), UNDERLYING_DECIMALS); + + // --- 1. APPROVE --- + console.log(`Approving ${ASSET_NAME} to be transferred to the c${ASSET_NAME} contract...`); + // NOTE: Use 'cTokenAddress' as the spender address for the underlying token (DAI). + let approveTx = await underlying.approve(CTOKEN_ADDRESS, amountToSupplyBN); + await approveTx.wait(); + console.log(`Approval successful. (Tx: ${approveTx.hash})\n`); + + // --- 2. MINT (SUPPLY) --- + console.log(`Supplying ${ASSET_NAME} to the protocol as collateral...`); + let mintTx = await cToken.mint(amountToSupplyBN); + const mintReceipt = await mintTx.wait(); + checkCompoundFailure(mintReceipt, 'Mint (Supply)'); + console.log(`Supply successful. (Tx: ${mintTx.hash})\n`); + + await logBalances(myWalletAddress, provider, cToken, underlying); + + // --- 3. ENTER MARKET --- + console.log('Entering market (via Comptroller contract) for cDAI as collateral...'); + const markets = [CTOKEN_ADDRESS]; // The cToken contract(s) for your collateral + let enterMarketsTx = await comptroller.enterMarkets(markets); + await enterMarketsTx.wait(); + console.log(`Entered market successfuly. (Tx: ${enterMarketsTx.hash})\n`); + + // --- 4. CALCULATE LIQUIDITY AND DISPLAY INFO --- + + // Get account liquidity (in ETH, scaled by 1e18) + const [_, liquidityBN] = await comptroller.getAccountLiquidity(myWalletAddress); + const liquidity = ethers.utils.formatEther(liquidityBN); // Format to standard ETH/DAI decimals (18) + + // Get collateral factor (as fixed-point number, scaled by 1e18) + const [__, collateralFactorBN] = await comptroller.markets(CTOKEN_ADDRESS); + const collateralFactor = ethers.utils.formatEther(collateralFactorBN.mul(100)); // Convert to percent + + // Get asset price (DAI price in USD, scaled by 1e6) + let underlyingPriceInUsdBN = await priceFeed.price(ASSET_NAME); + const underlyingPriceInUsd = ethers.utils.formatUnits(underlyingPriceInUsdBN, 6); // Price feed uses 6 decimals + + // Get ETH borrow rate (as fixed-point number, scaled by 1e18) + let borrowRateBN = await cEth.borrowRatePerBlock(); + const borrowRate = ethers.utils.formatEther(borrowRateBN); + + console.log(`--- Borrowing Capacity ---`); + console.log(`LIQUIDITY: ${liquidity} ETH (or USD value at current price) available for borrowing.`); + console.log(`COLLATERAL FACTOR: ${collateralFactor}% of your supplied value is counted as collateral.`); + console.log(`DAI PRICE: 1 ${ASSET_NAME} == ${parseFloat(underlyingPriceInUsd).toFixed(4)} USD`); + console.log(`BORROW RATE: ${borrowRate} ETH per block.`); + console.log(`\nWARNING: NEVER borrow near the maximum amount due to liquidation risk.`); + + // --- 5. BORROW --- + const ethToBorrowBN = ethers.utils.parseEther(AMOUNT_TO_BORROW.toString()); + console.log(`\nNow attempting to borrow ${AMOUNT_TO_BORROW} ETH...`); + + let borrowTx = await cEth.borrow(ethToBorrowBN); + const borrowReceipt = await borrowTx.wait(); + checkCompoundFailure(borrowReceipt, 'Borrow'); + console.log(`ETH borrow successful. (Tx: ${borrowTx.hash})\n`); + + await logBalances(myWalletAddress, provider, cToken, underlying); + + // Log current borrow balance + let borrowBalanceBN = await cEth.callStatic.borrowBalanceCurrent(myWalletAddress); + const borrowBalance = ethers.utils.formatEther(borrowBalanceBN); + console.log(`Current ETH Borrow Balance: ${borrowBalance} ETH`); + + console.log(`\n--- Transaction Complete ---\n`); + + // --- 6. REPAY --- + console.log(`Now repaying the borrow...`); + + // Repaying ETH requires sending ETH as value in the transaction + const repayBorrowTx = await cEth.repayBorrow({ + value: ethToBorrowBN // Repay the exact amount borrowed + }); + const repayReceipt = await repayBorrowTx.wait(); + + checkCompoundFailure(repayReceipt, 'Repay Borrow'); + + console.log(`Borrow repaid successfully. (Tx: ${repayBorrowTx.hash})\n`); + await logBalances(myWalletAddress, provider, cToken, underlying); }; +// Execute main function and catch any top-level errors main().catch((err) => { - console.error('ERROR:', err); + // Log the full stack trace for better debugging + console.error('CRITICAL ERROR:', err.stack || err.message); });