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);
}
}