Complete Projects

Features: campaigns with goal and deadline, pledges/unpledges, creator claims if goal met, backers refund otherwise.

42.1 Project: Crowdfunding (Kickstarter-style) with ERC-20 pledges

Features: campaigns with goal and deadline, pledges/unpledges, creator claims if goal met, backers refund otherwise.

Test ideas: time travel with vm.warp, fuzz pledge amounts, invariant "token balance == sum of unclaimed pledges".

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract CrowdFund {
    using SafeERC20 for IERC20;

    struct Campaign {
        address creator;
        uint128 goal;
        uint128 pledged;
        uint40 startAt;
        uint40 endAt;
        bool claimed;
    }

    IERC20 public immutable token;
    uint256 public count;
    uint256 public constant MAX_DURATION = 90 days;
    mapping(uint256 => Campaign) public campaigns;
    mapping(uint256 => mapping(address => uint256)) public pledgedAmount;

    event Launch(uint256 indexed id, address indexed creator, uint128 goal, uint40 startAt, uint40 endAt);
    event Cancel(uint256 indexed id);
    event Pledge(uint256 indexed id, address indexed caller, uint256 amount);
    event Unpledge(uint256 indexed id, address indexed caller, uint256 amount);
    event Claim(uint256 indexed id);
    event Refund(uint256 indexed id, address indexed caller, uint256 amount);

    error InvalidTimes();
    error NotCreator();
    error NotStarted();
    error Ended();
    error Not
…

42.2 Project: NFT marketplace (fixed-price listings, pull payments)

Extensions: EIP-712 off-chain signed listings (gasless listing, like Seaport), offers in WETH, auctions, ERC-1155 support.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import {IERC2981} from "@openzeppelin/contracts/interfaces/IERC2981.sol";
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

contract NFTMarketplace is ReentrancyGuard {
    struct Listing { address seller; uint96 price; }

    mapping(address nft => mapping(uint256 tokenId => Listing)) public listings;
    mapping(address => uint256) public proceeds;
    uint256 public constant FEE_BPS = 250;          // 2.5%
    address public immutable feeRecipient;

    event Listed(address indexed nft, uint256 indexed tokenId, address seller, uint256 price);
    event Cancelled(address indexed nft, uint256 indexed tokenId);
    event Bought(address indexed nft, uint256 indexed tokenId, address buyer, uint256 price);

    error NotOwner();
    error NotApproved();
    error NotListed();
    error WrongPrice();
    error NoProceeds();

    constructor(address _feeRecipient) { feeRecipient = _feeRecipient; }

    function list(address nft, uint256 tokenId
…

42.3 Project: Time-locked savings vault with transient reentrancy lock

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract TimeLockVault {
    struct Deposit { uint128 amount; uint64 unlockAt; }

    mapping(address => Deposit[]) public deposits;
    bool transient locked;

    event Deposited(address indexed user, uint256 index, uint256 amount, uint256 unlockAt);
    event Withdrawn(address indexed user, uint256 index, uint256 amount);

    error Locked();
    error Reentrant();
    error Empty();
    error BadDuration();

    modifier nonReentrant() {
        if (locked) revert Reentrant();
        locked = true;
        _;
        locked = false;
    }

    function deposit(uint64 lockSeconds) external payable {
        if (msg.value == 0) revert Empty();
        if (lockSeconds < 1 days || lockSeconds > 4 * 365 days) revert BadDuration();
        uint64 unlockAt = uint64(block.timestamp) + lockSeconds;
        deposits[msg.sender].push(Deposit(uint128(msg.value), unlockAt));
        emit Deposited(msg.sender, deposits[msg.sender].length - 1, msg.value, unlockAt);
    }

    function withdraw(uint256 index) external nonReentrant {
        Deposit storage d = deposits[msg.sender][index];
        if (d.amount == 0) revert Empty();
    
…

42.4 Foundry test for the vault

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {Test} from "forge-std/Test.sol";
import {TimeLockVault} from "../src/TimeLockVault.sol";

contract TimeLockVaultTest is Test {
    TimeLockVault vault;
    address alice = makeAddr("alice");

    function setUp() public {
        vault = new TimeLockVault();
        vm.deal(alice, 100 ether);
    }

    function test_CannotWithdrawEarly() public {
        vm.startPrank(alice);
        vault.deposit{value: 1 ether}(7 days);
        vm.expectRevert(TimeLockVault.Locked.selector);
        vault.withdraw(0);
        vm.stopPrank();
    }

    function testFuzz_WithdrawAfterUnlock(uint96 amount, uint64 lockSeconds) public {
        amount = uint96(bound(amount, 1, 100 ether));
        lockSeconds = uint64(bound(lockSeconds, 1 days, 4 * 365 days));
        vm.startPrank(alice);
        vault.deposit{value: amount}(lockSeconds);
        vm.warp(block.timestamp + lockSeconds);
        uint256 before = alice.balance;
        vault.withdraw(0);
        vm.stopPrank();
        assertEq(alice.balance, before + amount);
    }
}