DEV Community

rim dinov
rim dinov

Posted on

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 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

  1. 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");
}
Enter fullscreen mode Exit fullscreen mode

}

  1. 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; 
}
Enter fullscreen mode Exit fullscreen mode

}

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");
}
Enter fullscreen mode Exit fullscreen mode

}

  1. 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;
}
Enter fullscreen mode Exit fullscreen mode

}

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

}

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");
}
Enter fullscreen mode Exit fullscreen mode

}
⚙️ 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

Top comments (0)