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
285 changes: 167 additions & 118 deletions examples/repay-full-borrow-example.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
// Optimized Hardhat Test for Compound III Full Repayment

const assert = require('assert');
const { TASK_NODE_CREATE_SERVER } = require('hardhat/builtin-tasks/task-names');
const hre = require('hardhat');
const ethers = require('ethers');
const { BigNumber, Contract, utils } = require('ethers');
const { resetForkedChain } = require('./common.js');
const networks = require('./addresses.json');
const net = hre.config.cometInstance;

const jsonRpcUrl = 'http://127.0.0.1:8545';
const providerUrl = hre.config.networks.hardhat.forking.url;
const blockNumber = hre.config.networks.hardhat.forking.blockNumber;
// --- Configuration ---
const NET_NAME = hre.config.cometInstance;
const JSON_RPC_URL = 'http://127.0.0.1:8545';
const PROVIDER_URL = hre.config.networks.hardhat.forking.url;
const BLOCK_NUMBER = hre.config.networks.hardhat.forking.blockNumber;

// Asset decimal constants
const WETH_DECIMALS = 18;
const USDC_DECIMALS = 6;
const USDC_MANTISSA = BigNumber.from(10).pow(USDC_DECIMALS); // 1e6
const WETH_MANTISSA = BigNumber.from(10).pow(WETH_DECIMALS); // 1e18

const cometAbi = [
// --- ABIs ---
const COMET_ABI = [
'event Supply(address indexed from, address indexed dst, uint256 amount)',
'function supply(address asset, uint amount)',
'function withdraw(address asset, uint amount)',
Expand All @@ -19,200 +29,239 @@ const cometAbi = [
'function collateralBalanceOf(address account, address asset) external view returns (uint128)',
];

const wethAbi = [
const WETH_ABI = [
'function deposit() payable',
'function balanceOf(address) returns (uint)',
'function approve(address, uint) returns (bool)',
'function transfer(address, uint)',
];

const stdErc20Abi = [
const STD_ERC20_ABI = [
'function approve(address, uint) returns (bool)',
'function transfer(address, uint)',
];

const myContractAbi = [
const MY_CONTRACT_ABI = [
'function supply(address asset, uint amount) public',
'function withdraw(address asset, uint amount) public',
'function repayFullBorrow(address baseAsset) public',
];

let jsonRpcServer, deployment, cometAddress, myContractFactory, baseAssetAddress, wethAddress;
// --- Global Variables ---
let jsonRpcServer, myContractFactory;
let addresses, signer, provider;

// Contract addresses loaded from config
let cometAddress, usdcAddress, wethAddress;

// Contracts initialized later
let comet, weth, usdc;
let deployment;

// Derive addresses/keys from mnemonic
const deriveWallets = () => {
const mnemonic = hre.network.config.accounts.mnemonic;
const derivedAddresses = [];
const derivedPrivateKeys = [];
for (let i = 0; i < 20; i++) {
const wallet = new hre.ethers.Wallet.fromMnemonic(mnemonic, `m/44'/60'/0'/0/${i}`);
derivedAddresses.push(wallet.address);
derivedPrivateKeys.push(wallet._signingKey().privateKey);
}
return derivedAddresses;
};

// --- Helper Functions ---

const mnemonic = hre.network.config.accounts.mnemonic;
const addresses = [];
const privateKeys = [];
for (let i = 0; i < 20; i++) {
const wallet = new ethers.Wallet.fromMnemonic(mnemonic, `m/44'/60'/0'/0/${i}`);
addresses.push(wallet.address);
privateKeys.push(wallet._signingKey().privateKey);
/** Advances the EVM block height by a specified number of blocks. */
async function advanceBlockHeight(blocks) {
const txns = [];
for (let i = 0; i < blocks; i++) {
txns.push(hre.network.provider.send('evm_mine'));
}
await Promise.all(txns);
}

/**
* Seeds a target address with the Base Token (USDC) by using a helper account
* (address[9]) to supply WETH, borrow USDC, and transfer it.
*/
async function seedWithBaseToken(toAddress, amountUSD) {
const helperSigner = provider.getSigner(addresses[9]);
const helperComet = new Contract(cometAddress, COMET_ABI, helperSigner);
const helperWeth = new Contract(wethAddress, WETH_ABI, helperSigner);
const helperUsdc = new Contract(usdcAddress, STD_ERC20_ABI, helperSigner);

const wethSupply = utils.parseUnits('10', WETH_DECIMALS);
const usdcBorrow = utils.parseUnits('1000', USDC_DECIMALS); // Ensure it's above baseBorrowMin

// Helper setup: Deposit ETH for WETH
let tx = await helperWeth.deposit({ value: wethSupply });
await tx.wait(1);

// Helper setup: Approve and Supply WETH to Comet
tx = await helperWeth.approve(cometAddress, hre.ethers.constants.MaxUint256);
await tx.wait(1);
tx = await helperComet.supply(wethAddress, wethSupply);
await tx.wait(1);

// Helper: Borrow USDC
tx = await helperComet.withdraw(usdcAddress, usdcBorrow);
await tx.wait(1);

// Helper: Transfer required amount to the main test account
const amountToTransfer = utils.parseUnits(String(amountUSD), USDC_DECIMALS);
tx = await helperUsdc.transfer(toAddress, amountToTransfer);
await tx.wait(1);
}

// --- Test Suite ---

describe("Repay an entire Compound III account's borrow", function () {
before(async () => {
console.log('\n Running a hardhat local evm fork of a public net...\n');

addresses = deriveWallets();

// 1. Start Hardhat local EVM fork
console.log('\n\tRunning a hardhat local evm fork of a public net...\n');
jsonRpcServer = await hre.run(TASK_NODE_CREATE_SERVER, {
hostname: '127.0.0.1',
port: 8545,
provider: hre.network.provider
});

await jsonRpcServer.listen();

baseAssetAddress = networks[net].USDC;
usdcAddress = baseAssetAddress;
cometAddress = networks[net].comet;
wethAddress = networks[net].WETH;
// 2. Setup contract addresses
usdcAddress = networks[NET_NAME].USDC;
baseAssetAddress = usdcAddress; // Alias for clarity
cometAddress = networks[NET_NAME].comet;
wethAddress = networks[NET_NAME].WETH;

// 3. Get Contract Factory
myContractFactory = await hre.ethers.getContractFactory('MyContract');
});

beforeEach(async () => {
await resetForkedChain(hre, providerUrl, blockNumber);
// 1. Reset the chain state for a clean test
await resetForkedChain(hre, PROVIDER_URL, BLOCK_NUMBER);

// 2. Setup Ethers.js provider and main signer (address[0])
provider = new hre.ethers.providers.JsonRpcProvider(JSON_RPC_URL);
signer = provider.getSigner(addresses[0]);

// 3. Initialize contract instances
comet = new Contract(cometAddress, COMET_ABI, signer);
weth = new Contract(wethAddress, WETH_ABI, signer);
usdc = new Contract(usdcAddress, STD_ERC20_ABI, signer);

// 4. Deploy the wrapper contract
deployment = await myContractFactory.deploy(cometAddress);
});

after(async () => {
await jsonRpcServer.close();
});

// --- Test Case 1: Repaying using standard Ethers.js/Comet contract ---
it('Repays an entire borrow without missing latest block interest using JS', async () => {
const provider = new ethers.providers.JsonRpcProvider(jsonRpcUrl);
const signer = provider.getSigner(addresses[0]);
const comet = new ethers.Contract(cometAddress, cometAbi, signer);
const weth = new ethers.Contract(wethAddress, wethAbi, signer);
const usdc = new ethers.Contract(usdcAddress, stdErc20Abi, signer);
const baseAssetMantissa = 1e6; // USDC has 6 decimal places

let tx = await weth.deposit({ value: ethers.utils.parseEther('10') });
// WETH setup (Supply collateral)
const wethSupplyAmount = utils.parseUnits('10', WETH_DECIMALS);
let tx = await weth.deposit({ value: wethSupplyAmount });
await tx.wait(1);

console.log('\tApproving Comet to move WETH collateral...');
tx = await weth.approve(cometAddress, ethers.constants.MaxUint256);
tx = await weth.approve(cometAddress, hre.ethers.constants.MaxUint256);
await tx.wait(1);

console.log('\tSending initial supply to Compound...');
tx = await comet.supply(wethAddress, ethers.utils.parseEther('10'));
console.log('\tSending initial WETH supply to Compound...');
tx = await comet.supply(wethAddress, wethSupplyAmount);
await tx.wait(1);

// Accounts cannot hold a borrow smaller than baseBorrowMin (100 USDC).
// Initial Borrow
const borrowSize = 1000;
console.log('\tExecuting initial borrow of the base asset from Compound...');
console.log('\tBorrow size:', borrowSize);

// Do borrow
tx = await comet.withdraw(usdcAddress, (borrowSize * baseAssetMantissa).toString());
const borrowAmount = BigNumber.from(borrowSize).mul(USDC_MANTISSA);

console.log(`\tExecuting initial borrow of the base asset (USDC) of ${borrowSize} ...`);
tx = await comet.withdraw(usdcAddress, borrowAmount);
await tx.wait(1);

let borrowBalance = await comet.callStatic.borrowBalanceOf(addresses[0]);
console.log('\tBorrow Balance initial', +borrowBalance.toString() / baseAssetMantissa);
console.log('\tBorrow Balance initial:', +borrowBalance.toString() / USDC_MANTISSA.toNumber());

// accrue some interest
// Accrue interest
console.log('\tFast forwarding 100 blocks to accrue some borrower interest...');
await advanceBlockHeight(100);

borrowBalance = await comet.callStatic.borrowBalanceOf(addresses[0]);
console.log('\tBorrow Balance after some interest accrued', +borrowBalance.toString() / baseAssetMantissa);
console.log('\tBorrow Balance after interest accrued:', +borrowBalance.toString() / USDC_MANTISSA.toNumber());

// For example purposes, get extra USDC so we can pay off the
// original borrow plus the accrued borrower interest
await seedWithBaseToken(addresses[0], 5);
// Prepare for Repayment (get extra USDC to cover interest)
await seedWithBaseToken(addresses[0], 5); // Seed 5 USDC

tx = await usdc.approve(cometAddress, ethers.constants.MaxUint256);
// Approve the maximum amount of USDC for Comet to withdraw
tx = await usdc.approve(cometAddress, hre.ethers.constants.MaxUint256);
await tx.wait(1);

console.log('\tRepaying the entire borrow...');
tx = await comet.supply(usdcAddress, ethers.constants.MaxUint256);
// Full Repayment: Supply MaxUint256 of the base asset (USDC).
// This is the method for paying off the full accrued borrow balance.
console.log('\tRepaying the entire borrow by supplying MaxUint256 of the base asset...');
tx = await comet.supply(usdcAddress, hre.ethers.constants.MaxUint256);
await tx.wait(1);

borrowBalance = await comet.callStatic.borrowBalanceOf(addresses[0]);
console.log('\tBorrow Balance after full repayment', +borrowBalance.toString() / baseAssetMantissa);

// Assert the borrow balance is zero (or near zero, depending on timing)
const finalBalance = +borrowBalance.toString() / USDC_MANTISSA.toNumber();
assert.ok(finalBalance < 1e-4, `Borrow balance should be near zero after full repayment. Got: ${finalBalance}`);
console.log('\tBorrow Balance after full repayment:', finalBalance);
});

// --- Test Case 2: Repaying using a custom Solidity contract function ---
it('Repays an entire borrow without missing latest block interest using Solidity', async () => {
const me = addresses[0];
const provider = new ethers.providers.JsonRpcProvider(jsonRpcUrl);
const signer = provider.getSigner(me);
const comet = new ethers.Contract(cometAddress, cometAbi, signer);
const MyContract = new ethers.Contract(deployment.address, myContractAbi, signer);
const weth = new ethers.Contract(wethAddress, wethAbi, signer);
const wethMantissa = 1e18; // WETH and ETH have 18 decimal places
const usdc = new ethers.Contract(baseAssetAddress, stdErc20Abi, signer);
const baseAssetMantissa = 1e6; // USDC has 6 decimal places

let tx = await weth.deposit({ value: ethers.utils.parseEther('10') });
const MY_CONTRACT_ADDRESS = deployment.address;
const MyContract = new Contract(MY_CONTRACT_ADDRESS, MY_CONTRACT_ABI, signer);

// WETH setup (Supply collateral to the wrapper contract)
const wethSupplyAmount = utils.parseUnits('10', WETH_DECIMALS);
let tx = await weth.deposit({ value: wethSupplyAmount });
await tx.wait(1);

console.log('\tTransferring WETH to MyContract to use as collateral...');
tx = await weth.transfer(MyContract.address, ethers.utils.parseEther('10'));
tx = await weth.transfer(MY_CONTRACT_ADDRESS, wethSupplyAmount);
await tx.wait(1);

console.log('\tSending initial supply to Compound...');
tx = await MyContract.supply(wethAddress, ethers.utils.parseEther('10'));
// Supply WETH from MyContract to Comet
console.log('\tMyContract sending initial WETH supply to Compound...');
tx = await MyContract.supply(wethAddress, wethSupplyAmount);
await tx.wait(1);

// Accounts cannot hold a borrow smaller than baseBorrowMin (100 USDC).
// Initial Borrow
const borrowSize = 1000;
console.log('\tExecuting initial borrow of the base asset from Compound...');
console.log('\tBorrow size:', borrowSize);

// Do borrow
tx = await MyContract.withdraw(usdcAddress, (borrowSize * baseAssetMantissa).toString());
const borrowAmount = BigNumber.from(borrowSize).mul(USDC_MANTISSA);

console.log(`\tMyContract executing initial borrow of the base asset (USDC) of ${borrowSize}...`);
tx = await MyContract.withdraw(usdcAddress, borrowAmount);
await tx.wait(1);

// accrue some interest
// Accrue interest
console.log('\tFast forwarding 100 blocks to accrue some borrower interest...');
await advanceBlockHeight(100);

borrowBalance = await comet.callStatic.borrowBalanceOf(MyContract.address);
console.log('\tBorrow Balance after some interest accrued', +borrowBalance.toString() / baseAssetMantissa);
let borrowBalance = await comet.callStatic.borrowBalanceOf(MY_CONTRACT_ADDRESS);
console.log('\tBorrow Balance after interest accrued:', +borrowBalance.toString() / USDC_MANTISSA.toNumber());

// For example purposes, get extra USDC so we can pay off the
// original borrow plus the accrued borrower interest
await seedWithBaseToken(MyContract.address, 5);
// Prepare for Repayment (seed the MyContract address with extra USDC)
await seedWithBaseToken(MY_CONTRACT_ADDRESS, 5); // Seed 5 USDC

console.log('\tRepaying the entire borrow...');
// Repay: Call the custom function in the Solidity wrapper contract
console.log('\tMyContract repaying the entire borrow using its custom function...');
tx = await MyContract.repayFullBorrow(usdcAddress);
await tx.wait(1);

borrowBalance = await comet.callStatic.borrowBalanceOf(MyContract.address);
console.log('\tBorrow Balance after full repayment', +borrowBalance.toString() / baseAssetMantissa);
borrowBalance = await comet.callStatic.borrowBalanceOf(MY_CONTRACT_ADDRESS);

// Assert the borrow balance is zero (or near zero)
const finalBalance = +borrowBalance.toString() / USDC_MANTISSA.toNumber();
assert.ok(finalBalance < 1e-4, `Borrow balance should be near zero after full repayment. Got: ${finalBalance}`);
console.log('\tBorrow Balance after full repayment:', finalBalance);
});
});

async function advanceBlockHeight(blocks) {
const txns = [];
for (let i = 0; i < blocks; i++) {
txns.push(hre.network.provider.send('evm_mine'));
}
await Promise.all(txns);
}

// Test account index 9 uses Comet to borrow and then seed the toAddress with tokens
async function seedWithBaseToken(toAddress, amt) {
const baseTokenDecimals = 6; // USDC
const provider = new ethers.providers.JsonRpcProvider(jsonRpcUrl);
const signer = provider.getSigner(addresses[9]);
const comet = new ethers.Contract(cometAddress, cometAbi, signer);
const weth = new ethers.Contract(wethAddress, wethAbi, signer);
const usdc = new ethers.Contract(usdcAddress, stdErc20Abi, signer);

let tx = await weth.deposit({ value: ethers.utils.parseEther('10') });
await tx.wait(1);

tx = await weth.approve(cometAddress, ethers.constants.MaxUint256);
await tx.wait(1);

tx = await comet.supply(wethAddress, ethers.utils.parseEther('10'));
await tx.wait(1);

// baseBorrowMin is 1000 USDC
tx = await comet.withdraw(usdcAddress, (1000 * 1e6).toString());
await tx.wait(1);

// transfer from this account to the main test account (0th)
tx = await usdc.transfer(toAddress, (amt * 1e6).toString());
await tx.wait(1);

return;
}