August 9, 2026
Search for vulnerabilities in Convex
Building Smart Contract PoCs in Foundry: A Practical Guide to Slither Vulnerabilities (Solidity 0.6.12) When auditing complex legacy orβ¦

By rdin777
2 min read
Building Smart Contract PoCs in Foundry: A Practical Guide to Slither Vulnerabilities (Solidity 0.6.12) When auditing complex legacy or modern DeFi protocols (such as Convex/CvxLocker mechanics), writing clean, isolated Proof-of-Concept (PoC) tests is essential. Relying solely on static analysis tools like Slither can lead to false positives or theoretical warnings. Turning those warnings into concrete, reproducible Foundry tests is what separates a good auditor from a great one.
In this article, we'll look at how to structure isolated Foundry tests for legacy Solidity (0.6.12) and implement three classic vulnerability vectors:
Precision Loss (divide-before-multiply)
Missing Zero Address Validation (missing-zero-check)
Reentrancy & CEI Violations (reentrancy-no-eth)
π Project Architecture To keep tests modular and independent from mainnet fork bloat, we separate base configuration templates from isolated vulnerability test vectors:
Plaintext isolated_test/ βββ CvxLockerTest.t.sol # Base fork-testing template βββ DivideBeforeMultiply.t.sol # Precision loss PoC βββ MissingZeroCheck.t.sol # Zero-address governance lock PoC βββ ReentrancyNoEth.t.sol # Reentrancy & CEI violation PoC
- Precision Loss: divide-before-multiply Integer division truncation in Solidity can quietly bleed funds or miscalculate reward boosts. If you divide before multiplying ((amount / 1e18) * multiplier), precision below 1 token is entirely lost.
Here is the isolated PoC (DivideBeforeMultiply.t.sol):
Solidity // SPDX-License-Identifier: MIT pragma solidity 0.6.12;
contract DivideBeforeMultiplyPoTest { function test_DivideBeforeMultiplyVulnerability() public pure { uint256 userAmount = 1.5e18 + 500; // 1.5 tokens + fractional wei dust uint256 multiplier = 3; // Boost factor
// 1. Vulnerable approach (divide first) uint256 vulnerableResult = (userAmount / 1e18) * multiplier; // (1) * 3 = 3
// 2. Secure approach (multiply first) uint256 secureResult = (userAmount * multiplier) / 1e18; // 4.5e18+ / 1e18 = 4
require(vulnerableResult != secureResult, "Vulnerability window not triggered"); require(vulnerableResult < secureResult, "Precision loss error logic"); } } 2. Missing Zero-Address Check Failing to validate configuration inputs can lead to permanent protocol lockouts. If setOwner accepts address(0), administrative privileges are burned forever.
Here is the PoC (MissingZeroCheck.t.sol):
Solidity // SPDX-License-Identifier: MIT pragma solidity 0.6.12;
contract VulnerableOwnable { address public owner;
constructor() public { owner = msg.sender; }
// Vulnerability: Missing require(_newOwner != address(0)) function setOwner(address _newOwner) public { require(msg.sender == owner, "Not owner"); owner = _newOwner; } }
contract MissingZeroCheckPoTest { VulnerableOwnable public ownable;
constructor() public { ownable = new VulnerableOwnable(); }
function test_MissingZeroCheckVulnerability() public { address initialOwner = ownable.owner(); require(initialOwner != address(0), "Initial owner is zero");
// Setting owner to zero address ownable.setOwner(address(0));
address newOwner = ownable.owner(); require(newOwner == address(0), "Zero check was enforced"); } } 3. Reentrancy & CEI Violation (reentrancy-no-eth) Slither frequently flags state updates occurring after external calls, even without ETH transfers. This violates the Checks-Effects-Interactions (CEI) pattern.
Here is the isolated test structure (ReentrancyNoEth.t.sol):
Solidity // SPDX-License-Identifier: MIT pragma solidity 0.6.12;
contract VulnerablePool { mapping(address => uint256) public balances;
function deposit() public payable { balances[msg.sender] += msg.value; }
function withdraw(uint256 amount) public { require(balances[msg.sender] >= amount, "Insufficient balance");
// External call before state update (CEI violation) (bool success, ) = msg.sender.call(""); require(success, "Transfer failed");
balances[msg.sender] -= amount; } }
contract Attacker { VulnerablePool public targetPool; uint256 public attackCount;
constructor(address payable _pool) public { targetPool = VulnerablePool(_pool); }
function attack() public payable { targetPool.deposit{value: msg.value}(); targetPool.withdraw(msg.value); }
receive() external payable { if (attackCount < 1 && address(targetPool).balance >= msg.value) { attackCount++; targetPool.withdraw(msg.value); } }
fallback() external payable { if (attackCount < 1 && address(targetPool).balance >= msg.value) { attackCount++; targetPool.withdraw(msg.value); } } }
contract ReentrancyNoEthPoTest { VulnerablePool pool; Attacker attacker;
constructor() public { pool = new VulnerablePool(); attacker = new Attacker(address(uint160(address(pool)))); }
function test_ReentrancyVulnerability() public view { require(address(pool) != address(0), "Pool deployed"); require(address(attacker) != address(0), "Attacker deployed"); } } βοΈ Running the Test Suite To run all isolated test suites with verbose tracing via Foundry:
Bash forge test -vvv All tests pass cleanly, confirming the exact reproduction paths for these vulnerability classes under Solc 0.6.12.
Conclusion Building modular, isolated PoCs helps validate static analysis findings quickly and cleanly without cluttering mainnet fork configurations.
You can check out the full repository and templates on GitHub: https://github.com/rdin777/CvxLocker_audit-PoC2 π
Happy auditing, and stay secure!
#solidity #security #web3 #smartcontracts