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.