Instructions: Each contract below contains at least one critical vulnerability. Your task is to identify the vulnerability, explain how it can be exploited, and propose a fix. Use the vulnerability reference guide to help identify patterns.

Main Contracts (Required)

Contract 1: Bank Withdrawal System Difficulty: Medium
A simple banking contract that allows users to deposit and withdraw ETH. Users can check their balance and withdraw funds at any time.
contract Bank {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function getBalance() public view returns (uint256) {
        return balances[msg.sender];
    }

    function withdraw(uint256 amount) public {
        require(balances[msg.sender] >= amount, "Insufficient balance");

        // Transfer funds to user
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");

        // Update balance
        balances[msg.sender] -= amount;
    }
}
Contract 2: Token Ownership Manager Difficulty: Easy
A governance token contract where the owner can mint new tokens, pause transfers, and update critical parameters. The owner role is essential for protocol management.
contract TokenManager {
    address public owner;
    bool public paused;
    uint256 public totalSupply;
    mapping(address => uint256) public balances;

    constructor() {
        owner = msg.sender;
        totalSupply = 1000000;
        balances[msg.sender] = totalSupply;
    }

    function mint(address to, uint256 amount) public {
        require(msg.sender == owner, "Only owner can mint");
        totalSupply += amount;
        balances[to] += amount;
    }

    function setPaused(bool _paused) public {
        require(msg.sender == owner, "Only owner");
        paused = _paused;
    }

    function setOwner(address newOwner) public {
        // Transfer ownership to new address
        owner = newOwner;
    }

    function transfer(address to, uint256 amount) public {
        require(!paused, "Transfers paused");
        require(balances[msg.sender] >= amount, "Insufficient balance");
        balances[msg.sender] -= amount;
        balances[to] += amount;
    }
}
Contract 3: Simple Token Transfer Difficulty: Easy
A minimal token implementation supporting basic transfer operations. Users can transfer tokens between accounts.
contract SimpleToken {
    mapping(address => uint256) public balances;
    uint256 public totalSupply;

    constructor() {
        totalSupply = 1000000;
        balances[msg.sender] = totalSupply;
    }

    function transfer(address to, uint256 amount) public {
        // Deduct from sender
        balances[msg.sender] -= amount;

        // Add to recipient
        balances[to] += amount;
    }

    function balanceOf(address account) public view returns (uint256) {
        return balances[account];
    }
}
🏆 Bonus Contracts (Advanced) 🏆

Complete these for extra credit! These involve more sophisticated vulnerabilities.

Contract 4: Flash Loan Lending Pool Difficulty: Hard
A DeFi lending pool that offers flash loans (borrow and repay in same transaction). Interest rates adjust based on pool utilization.
contract LendingPool {
    mapping(address => uint256) public deposits;
    uint256 public totalLiquidity;
    uint256 public constant FEE = 3; // 0.3% fee

    function deposit() public payable {
        deposits[msg.sender] += msg.value;
        totalLiquidity += msg.value;
    }

    function flashLoan(uint256 amount) public {
        uint256 balanceBefore = address(this).balance;
        require(balanceBefore >= amount, "Insufficient liquidity");

        // Send borrowed amount to caller
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");

        // Expect repayment plus fee
        uint256 fee = (amount * FEE) / 1000;
        require(address(this).balance >= balanceBefore + fee, "Loan not repaid");
    }

    function withdraw(uint256 amount) public {
        require(deposits[msg.sender] >= amount, "Insufficient balance");

        // Calculate share of pool
        uint256 share = (amount * address(this).balance) / totalLiquidity;

        deposits[msg.sender] -= amount;
        totalLiquidity -= amount;

        (bool success, ) = msg.sender.call{value: share}("");
        require(success, "Withdrawal failed");
    }
}
Contract 5: Price Oracle Aggregator Difficulty: Hard
A price oracle that aggregates data from multiple DEX pools to provide accurate token pricing for lending protocols and derivatives.
contract PriceOracle {
    address public dexPool1;
    address public dexPool2;

    function getPrice(address token) public view returns (uint256) {
        // Get prices from two DEX pools
        uint256 price1 = IDex(dexPool1).getSpotPrice(token);
        uint256 price2 = IDex(dexPool2).getSpotPrice(token);

        // Return average price
        return (price1 + price2) / 2;
    }
}

contract LendingProtocol {
    PriceOracle public oracle;
    mapping(address => uint256) public collateral;
    mapping(address => uint256) public debt;

    function borrow(address token, uint256 amount) public {
        uint256 collateralValue = collateral[msg.sender];
        uint256 price = oracle.getPrice(token);
        uint256 borrowValue = amount * price;

        // Require 150% collateralization
        require(collateralValue * 100 >= borrowValue * 150, "Undercollateralized");

        debt[msg.sender] += amount;
        // Transfer borrowed tokens...
    }
}
Contract 6: Time-Locked Vault Difficulty: Medium-Hard
A vault contract that locks funds until a specific timestamp. Used for vesting schedules and timed releases.
contract TimeLockVault {
    mapping(address => uint256) public lockedAmount;
    mapping(address => uint256) public unlockTime;

    function deposit(uint256 lockDuration) public payable {
        require(msg.value > 0, "Must deposit funds");

        lockedAmount[msg.sender] += msg.value;
        unlockTime[msg.sender] = block.timestamp + lockDuration;
    }

    function withdraw() public {
        require(lockedAmount[msg.sender] > 0, "No funds locked");
        require(block.timestamp >= unlockTime[msg.sender], "Funds still locked");

        uint256 amount = lockedAmount[msg.sender];
        lockedAmount[msg.sender] = 0;

        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }

    function extendLock(uint256 additionalTime) public {
        require(lockedAmount[msg.sender] > 0, "No funds locked");
        unlockTime[msg.sender] = block.timestamp + additionalTime;
    }
}

© Joerg Osterrieder 2025-2026. All rights reserved.