Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
325 changes: 178 additions & 147 deletions examples-js/ethers-js/borrow-eth-with-erc20-collateral.js
Original file line number Diff line number Diff line change
@@ -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);
});