0

Pretty new to solidity and trying to test a DeFi-Bank smart contract that allows depositors to deposit and withdrawl usdc.

I have 4 tests and 3/4 are coming back with this error when I run the test and I can't figure it out at all. any advice would be much appreciated!

Smart contract:

// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0;

import "./IERC20.sol"; import "./ILendingPool.sol";


contract DeFiBank {
    address[] public depositors;
    uint public immutable amount;
    uint public immutable fee;
    bool public isWithdrawing;
    mapping(address => uint) public bankBalance;
    mapping(address => bool) public hasDeposited;

    // AAVE v2 lending pool
    ILendingPool pool = ILendingPool(0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9);
    // the USDC stablecoin
    IERC20 usdc = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
    // aave interest bearing USDC
    IERC20 aUsdc = IERC20(0x9bA00D6856a4eDF4665BcA2C2309936572473B7E);

    constructor(uint _amount) {
        amount = _amount;
        // the bank will take a 0.5% fee on all amounts deposits 
        fee = amount * 5 / 1000;
    }

    function deposit() public payable {
        // transfer usdc from the depositor to the bank smartcontract to be deposited
        bool success = usdc.transferFrom(msg.sender, address(this), amount);

        // ensure the return value of the 'transferfrom' function is successful
        require(success, "transfer of funds failed");

        // bank will subtract their fee so ony the difference will be deposited in the pool
        // update the amount that will be deposited into the lending pool
        uint newAmount = (amount - fee);

        // approve the bank smartcontract transferring amount to aave lending pool
        // require statement used to check if the approval was successful or not 
        // and will stop the function from executing if so
        require(usdc.approve(address(pool), newAmount), 'approval failed');

        // deposit usdc into aave lending pool
        pool.deposit(address(usdc), newAmount, address(this), 0);

        // update depositors bank balance in the mapping
        bankBalance[msg.sender] = bankBalance[msg.sender] + newAmount;

        // add user to the list of depositors if they haven't already deposited
        if(!hasDeposited[msg.sender]) {
            depositors.push(msg.sender);
        }

        //update deposited status to keep track
        hasDeposited[msg.sender] = true;
    }
    // function to enable depositors to withdraw only their interest earned
    function withdrawlInterest() public {
        uint interest = (aUsdc.balanceOf(address(this)) - (amount));

        // approve the bank smartcontract spending the interest bearing aUsdc token
        // require statement used to check if the approval was successful or not 
        require(aUsdc.approve(address(pool), interest), "approval failed");

        // prevent reentrancy
        require(!isWithdrawing, "reentrancy detected");
        isWithdrawing = true;

        // iterate through all the depositors and allow any individual depositor to withdrawl any interest earned
        // but still keep the initial amount deposited within the bank
        // this will be an external call to withdrawl the interest on each depositor once and once only
        // this will prevent the external call from being made multiple times over each iteration potentially causing an unintended behavior or an infinite loop
        // the returned value will be saved in the array 'withdrawn'to check the amount withdrawn is the right amount
        // before using that amount to update the depsitors bank balance
        uint[] memory withdrawn = new uint[](depositors.length);
        for(uint i = 0; i < depositors.length; i++) {
            withdrawn[i] = pool.withdraw(address(usdc), interest, depositors[i]);

            // check that the withdrawal was successful and the correct amount was withdrawn i.e. only the interest
            // <= instead of the strict equality operator ==
            // This is so if the variable withdrawn[i] is less than interest for any reason, the function will still execute and the user's can still withdrawl.
            require(withdrawn[i] <= interest, "Incorrect amount withdrawn");
        }
        isWithdrawing = false;

        for(uint i = 0; i < depositors.length; i++) {
            // only withdrawl interest if interest has accrued
            if(withdrawn[i] > 0) {
                // update users bank balance
                bankBalance[depositors[i]] = bankBalance[depositors[i]] - withdrawn[i]; 
            }
        }
    }

    // function to allow depositors to withdraw their total balance(initial deposit + interest)
    function withdrawBalance() public {
        uint balance = aUsdc.balanceOf(address(this));

        // require statement used to check if the approval was successful or not 
        require(aUsdc.approve(address(pool), balance));

        // prevent reentrancy
        require(!isWithdrawing, "reentrancy detected");
        isWithdrawing = true;

        uint[] memory withdrawn2 = new uint[](depositors.length);
        for(uint i = 0; i < depositors.length; i++) {
            withdrawn2[i] = pool.withdraw(address(usdc), balance, depositors[i]);

            // check that the withdrawal was successful and the correct amount was withdrawn i.e. the entire balance
            require(withdrawn2[i] <= balance, "Incorrect amount withdrawn");
        }
        isWithdrawing = false;

        for(uint i = 0; i < depositors.length; i++) {
            // update their balance to 0
            bankBalance[depositors[i]] = 0;

            // delete depositor if they've withdrawn their entire balance
            delete depositors[i];
        }  
    } } 

JS test:

const { expect, assert } = require("chai");
const { ethers, faucet } = require("hardhat");

describe('DeFiBank', function () {
  let deFiBank;
  let usdc;
  let aUsdc;
  let depositors;
  const amount = ethers.utils.parseEther('100');
  
  this.beforeEach(async () => {
    usdc = await ethers.getContractAt("IERC20", "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
    aUsdc = await ethers.getContractAt("IERC20", "0x9bA00D6856a4eDF4665BcA2C2309936572473B7E");
    pool = await ethers.getContractAt("ILendingPool", "0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9");

    depositors = (await ethers.provider.listAccounts()).slice(1, 4);

    const DeFiBank = await ethers.getContractFactory("DeFiBank");
    deFiBank = await DeFiBank.deploy(amount);
    await deFiBank.deployed();    

  });

  it('should set the banks take fee', async () => {
    const fee = await deFiBank.fee();
    assert(fee);
  });

  it('should deposit usdc into bank smart contract and update depositors balance', async () => {
    for (let i = 0; i < depositors.length; i++) {
      const signer = await ethers.provider.getSigner(depositors[i]);
      // ensure each depositor has approved the contract to transfer amount to smart contract
      await usdc.connect(signer).approve(deFiBank.address, amount);
      // depositors intial usdc balance
      const initialUsdcBalance = await usdc.balanceOf(depositors[i]);
      // call deposit function
      await deFiBank.connect(signer).deposit({value: amount});
      // depositors new usdc balance
      const finalUsdcBalance = await usdc.balanceOf(depositors[i]);
      // Check that the depositor's USDC balance decreases by the deposit amount
      expect.equal(finalUsdcBalance.toString(), (initialUsdcBalance.sub(amount)).toString());
      // check the transfer was successful
      const contractUsdcBalance = await usdc.balanceOf(deFiBank.address);
      expect.equal(contractUsdcBalance.toString(), (contractUsdcBalance.add(amount).toString()));
      // revert if transfer was unsuccessful
      await assert.reverts(deFiBank.connect(signer).deposit({value: amount}), "transfer of funds failed");
      // approve and deposit the usdc into the leding pool
      const fee = await deFiBank.fee();
      assert(fee);
      // bank only deposits amount - fees
      const newAmount = amount - fee;
      await usdc.connect(signer).approve(pool.address, newAmount);
      // revert if approval fails
      await assert.reverts(usdc.connect(signer).approve(pool.address, newAmount), "approval failed");
      // check that the DeFiBank contract now has an aUsdc balance
      const contractAUsdcBalance = await aUsdc.balanceOf(deFiBank.address);
      expect((contractAUsdcBalance.gte(newAmount.mul(depositors.length))).toString());
      // Check that the depositor's bank balance(mapping) increases by 
      // (amount - fee) after deposit() is called
      const initalBankBalance = await deFiBank.bankBalance(depositors[i]);
      const finalBankBalance = await deFiBank.bankBalance(depositors[i]);
      expect.equal(finalBankBalance.toString(), (initalBankBalance.add(newAmount)).toString());
      // check the depositor has been added to the list of depositors if they have not already deposited
      const depositorAdded = await deFiBank.hasDeposited(depositors[i]);
      assert.isTrue(depositorAdded);
    }
  });
  
  it('should withdrawal Interest and update deopsitors balance', async () => {
    for(let i = 0; i < depositors.length; i++) {
      const signer = await ethers.provider.getSigner(depositors[i]);
      const initBankAusdcBalance = await aUsdc.balanceOf(deFiBank.address);
      const interest = await (aUsdc.balanceOf(deFiBank.address)).sub(initBankAusdcBalance);
      const finBankAusdcBalance = await aUsdc.balanceOf(deFiBank.address);

      // ensure depositors have approved the pool to spend their aUsdc to transfer the interest earned
      await aUsdc.connect(signer).approve(pool.address, interest);
      // revert transaction if the approval failed
      await assert.reverts(aUsdc.connect(signer).approve(pool.address, interest), "approval failed");

      // withdrawl interest
      const withdrawn = await pool.connect(signer).withdrawlInterest({value: interest});
      // check the withdrawn amount is equal to expected amount
      assert.isTrue(withdrawn == interest);
      // revert if amount withdrawn is not equal to interest rate
      await assert.reverts(pool.connect(signer).withdrawlInterest({value: interest}), "Incorrect amount withdrawn");

      // check the bank contract's intial aUsdc balance decreases by the interest amount
      expect.equal(finBankAusdcBalance, initBankAusdcBalance.sub(interest));
      
      // check that the depositers usdc balance increases by the interest amount
      const initDepositorUsdcBalance = await usdc.balanceOf(depositors[i]);
      const finDepositorUsdcBalance = await usdc.balanceOf(depositors[i]);
      expect.equal(finDepositorUsdcBalance, initDepositorUsdcBalance.add(interest));

      // Check that the depositor's bank balance(mapping) decreases by 
      // the interest amount after withdrawlInterest() has been called only if interest > 0
      if(interest > 0) {
        const initlBankBalance = await deFiBank.bankBalance(depositors[i]);
        const finBankBalance = await deFiBank.bankBalance(depositors[i]);
        expect.equal(finBankBalance, initlBankBalance.sub(interest));
      }
      // check to make sure depositor can't withdraw interest more than once by calling the function recursively
      await pool.connect(signer).withdrawlInterest({value: interest});
      await assert.reverts(pool.connect(signer).withdrawlInterest({value: interest}), "reentrancy detected");
    }
  });

  it('should withdrawl total usdc balance and update depositors bank balance', async () => {
    for(let i = 0; i < depositors.length; i++) {
      const signer = await ethers.provider.getSigner(depositors[i]);
      const initialBankAusdcBalance = await aUsdc.balanceOf(deFiBank.address);
      const interest = await (aUsdc.balanceOf(deFiBank.address)).sub(initialBankAusdcBalance);

      // ensure depositors have approved the pool to spend their aUsdc to transfer their total balance
      await aUsdc.connect(signer).approve(pool.address, (initialBankAusdcBalance + interest));
      // revert transaction if the approval failed
      await assert.reverts(aUsdc.connect(signer).approve(pool.address, (initialBankAusdcBalance + interest), "approval failed"));

      // withdrawl total balance
      const withdrawl = await pool.connect(signer).withdrawlBalance({value: amount + interest});
      // check the withdrawn amount is equal to expected amount
      assert.isTrue(withdrawl == (initialBankAusdcBalance + interest));
      // revert if amount withdrawn is not equal to expected amount
      await assert.reverts(pool.connect(signer).withdrawlBalance({value: initialBankAusdcBalance}), "Incorrect amount withdrawn");

      // check the bank contract's intial aUsdc balance decreases by the interest amount
      const finalBankAusdcBalance = await aUsdc.balanceOf(deFiBank.address);
      expect.equal(finalBankAusdcBalance, initialBankAusdcBalance.sub(amount + interest));
      
      // check that the depositers usdc balance increases by the interest amount
      const initialDepositorUsdcBalance = await usdc.balanceOf(depositors[i]);
      const finalDepositorUsdcBalance = await usdc.balanceOf(depositors[i]);
      expect.equal(finalDepositorUsdcBalance, initialDepositorUsdcBalance.add(amount + interest));

      // Check that the depositor's bank balance(mapping) should be 0 
      const BankBalance = await deFiBank.bankBalance(depositors[i]);
      expect.equal(bankBalance, 0);
      
      // check to make sure depositor can't withdraw balance more than once 
      // by calling the function recursively e.g. reentrancy attack
      await pool.connect(signer).withdrawlBalance({value: amount + interest});
      await assert.reverts(pool.connect(signer).withdrawlBalance({value: amount + interest}), "reentrancy detected");
    }
  });
});

the error is with the last 3 tests: deposit, withdrawlInterest and withdrawlBalance functons.

1 Answer 1

0

From your code i assume that you need to fork ethereum and you probably aren't.

Your post also is lacking information, I dont know what tooling are you using.

If you use hardhat here is handy link for you: https://hardhat.org/hardhat-network/docs/guides/forking-other-networks

You will need node provider with ethereum archive node. One of good node providers that i can recommend is: https://www.alchemy.com

3
  • thanks for your quick response! I'll review the links you sent over and see if they are helpful! Could you explain why you think I need to fork the mainnet just so I can clarify my understanding. In terms of tooling, I deployed to goerli test net via alchemy; development/testing environment is hardhat; via VScode if thatt's what you mean by tooling? sorry i'm still very new to this...hope that provided more info for you
    – 0xJI
    Jan 17, 2023 at 14:27
  • Yes I meant hardhat, hardhat is awesome. You need to fork mainnet for development purposes because you run tests against your local env, and you are using USDC contract using mainnet address. You can additionally deploy your own USDC or ERC20 and use as your token contract, but if you plan to use USDC then its better to fork and use the actual thing. Its also worth noting that you will need to exchange your local eth to USDC on some DEX to have some to operate. I'm a new contributor to the space, any upvotes will gratitude my time, thank you :) Jan 19, 2023 at 5:01
  • If you want some guidance in forking or DEX swap you can contract me or create new question and send me the link Jan 19, 2023 at 5:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.