Back to Articles

24 Hard-Learned Lessons from Smart Contract Hacking Challenges

[ View on GitHub ]

24 Hard-Learned Lessons from Smart Contract Hacking Challenges

Hook

A single uninitialized variable in a proxy contract can hand over complete ownership of millions in assets. Mario Poneder's security take-aways repository documents 24 such landmines that even experienced Solidity developers regularly step on.

Context

Smart contract security isn't learned from documentation—it's earned through painful discoveries of edge cases, subtle vulnerabilities, and creative attack vectors that compilers won't catch. While formal verification tools and audit frameworks exist, there's immense value in curated wisdom from someone who has systematically broken vulnerable contracts to understand how they fail.

Mario Poneder created this repository after working through prominent security challenge platforms like Damn Vulnerable DeFi and Ethernaut, extracting the core lessons into a reference guide. Unlike academic security papers or comprehensive audit frameworks, this is a practitioner's field notes: terse, practical observations about the specific gotchas that trip up developers building on Ethereum. It exists in the gap between "here's the Solidity syntax" and "here's a 50-page security audit methodology"—a quick-reference checklist of the mistakes that actually happen in production code.

Technical Insight

The repository organizes 24 security pitfalls ranging from Solidity language quirks to sophisticated DeFi attack patterns. What makes this collection valuable isn't comprehensiveness—it's the focus on non-obvious vulnerabilities that pass code review but create exploitable conditions.

Consider take-away #11 on the extcodesize constructor bypass. Many contracts use address checks like this for access control:

function restrictedFunction() external {
    require(msg.sender.code.length == 0, "No contracts allowed");
    // Privileged operation
}

Developers implement this thinking it prevents contract-based attacks. But Poneder highlights a critical detail: during a contract's constructor execution, extcodesize returns zero because the runtime bytecode hasn't been stored yet. An attacker can trivially bypass this check:

contract Exploiter {
    constructor(address target) {
        // At this point, address(this).code.length == 0
        VulnerableContract(target).restrictedFunction();
    }
}

This isn't theoretical—it's a pattern that appears in production contracts attempting to restrict functions to EOAs (externally owned accounts).

The list also tackles DeFi-specific vulnerabilities like flash loan price manipulation attacks (take-away #8). Many protocols naively check spot prices from decentralized exchanges for oracles or collateral valuations. An attacker can manipulate these within a single transaction:

  1. Take a flash loan of millions in token A
  2. Swap massively on a DEX, distorting the price ratio
  3. Exploit the vulnerable protocol using the manipulated price
  4. Reverse the swap and repay the flash loan
  5. Profit from the arbitrage—all atomically in one transaction

Poneder emphasizes using time-weighted average prices (TWAPs) or decentralized oracle networks instead of spot prices for any security-critical logic.

Take-away #19 addresses proxy pattern vulnerabilities, particularly storage collisions between proxy and implementation contracts. When using delegatecall-based proxies, both contracts share the same storage space. If storage layouts don't align precisely, the proxy's state variables can collide with the implementation's:

// Proxy contract
contract Proxy {
    address public implementation; // slot 0
    address public admin;          // slot 1
    
    fallback() external payable {
        // delegatecall to implementation
    }
}

// Implementation (vulnerable)
contract Implementation {
    uint256 public someValue;      // slot 0 - COLLISION!
    mapping(address => uint) public balances; // slot 1 - COLLISION!
}

Modifying someValue in the implementation actually overwrites the implementation address in the proxy's storage. OpenZeppelin's TransparentUpgradeableProxy pattern solves this by reserving proxy storage slots, but developers building custom proxies frequently miss this subtlety.

Another non-obvious issue is take-away #6 on preserving msg.value through delegatecall chains. Unlike msg.sender which changes with each call, msg.value remains constant even through multiple delegatecalls. If a contract checks msg.value but is called via delegatecall from a payable function, an attacker can reuse the same ETH value across multiple logical operations, potentially bypassing payment checks or double-counting deposits.

The repository also covers pseudorandomness vulnerabilities (#4), warning against using block properties for randomness. Block timestamp, number, and hash are all manipulable by miners within certain constraints, making them unsuitable for lottery-style contracts or NFT reveals. Chainlink VRF or commit-reveal schemes are suggested alternatives.

Gotcha

The repository's minimalist format is both its strength and limitation. Each take-away is essentially a one-liner with a title—there are no code examples, attack scenarios, or remediation patterns included. You're getting the "what" without much of the "how" or "why." For developers encountering these concepts for the first time, the brief descriptions may not provide enough context to recognize the vulnerability in their own code or understand exploitation mechanics.

The list also lacks severity classification or risk scoring. Not all 24 issues carry equal weight—a pseudorandomness vulnerability in a toy lottery contract isn't comparable to a flash loan price manipulation that could drain a protocol's entire TVL. Developers working under time constraints would benefit from knowing which vulnerabilities to prioritize. Additionally, since this represents knowledge captured from specific CTF challenges completed at a point in time, it may not cover the latest attack vectors or Solidity version-specific mitigations. The Solidity language has evolved significantly, with 0.8.x introducing checked arithmetic by default and other security improvements.

Verdict

Use if: You're conducting smart contract security reviews and need a rapid-fire checklist of common pitfalls to verify against, preparing for security CTF challenges like Ethernaut or Damn Vulnerable DeFi, or onboarding developers to smart contract security and want a concise list of "gotchas" as a starting point before diving into comprehensive resources. This repository shines as a quick reference card for experienced developers who already understand the underlying concepts but want a reminder of edge cases. Skip if: You need detailed explanations with exploit code and remediation examples, want automated tooling to detect these issues in your codebase (use Slither, Mythril, or Securify instead), require a comprehensive security framework with severity ratings and audit methodologies (look to Trail of Bits' guides or ConsenSys best practices), or are completely new to Solidity security (start with CryptoZombies and Solidity documentation first, then return to this as a reference).

// ADD TO YOUR README
[![Featured on Starlog](https://starlog.is/api/badge/cybersecurity/marioponeder-smart-contract-security-take-aways.svg)](https://starlog.is/api/badge-click/cybersecurity/marioponeder-smart-contract-security-take-aways)