diff --git a/examples/repay-full-borrow-example.js b/examples/repay-full-borrow-example.js index b1560ff..1ca2da2 100644 --- a/examples/repay-full-borrow-example.js +++ b/examples/repay-full-borrow-example.js @@ -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)', @@ -19,56 +29,131 @@ 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); }); @@ -76,143 +161,107 @@ describe("Repay an entire Compound III account's borrow", function () { 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; -} \ No newline at end of file