A production-ready decentralized crowdfunding protocol built on Ethereum using Solidity 0.8.20 and OpenZeppelin v5. The project includes a full educational progression of contracts, a TypeScript toolchain, and a comprehensive test suite.
The protocol allows any Ethereum account to create and fund crowdfunding projects. The following rules are enforced on-chain:
- Any account can create a project by providing a unique
id, aname, adescription, and afundraisingGoalgreater than zero. - A project starts in the
Openedstate. - Projects are stored in an array and referenced by their index.
- The account that creates a project becomes its author.
- Any account except the project author can fund an
Openedproject by sending ETH. - The ETH is transferred immediately and directly to the project author at the time of each contribution — the contract does not hold funds.
- The sent value must be greater than zero.
- A
Closedproject cannot receive funds. - Every contribution is recorded in the contract's contribution history, queryable by project
id.
- Only the project author can change a project's state.
- A project can be toggled between
OpenedandClosedat any time by its author. - Setting a project to its current state is not allowed.
- The contract owner (deployer by default) can
pause()the contract at any time. - While paused,
createProject()andfundProject()are blocked for all accounts. - The owner can
unpause()to restore normal operation.
- The deployer is the initial owner.
- Ownership can be transferred via
transferOwnership(). - Only the current owner can call admin functions (
pause,unpause,transferOwnership).
FirstSmartContract/
├── contracts/
│ ├── core/
│ │ └── CrowdFundingOZ.sol ← Production contract
│ ├── interfaces/
│ │ └── ICrowdFunding.sol ← Public interface
│ └── learning/
│ ├── project/ ← Educational progression (CrowdFunding1–6)
│ ├── enums/ ← Enum examples
│ ├── errors/ ← Custom error examples
│ ├── events/ ← Event examples
│ ├── functionModifiers/ ← Modifier examples
│ ├── functions/ ← Function visibility examples
│ └── variables/ ← Variable type examples
├── scripts/
│ └── deployCrowdFundingOZ.ts ← Deployment script
├── test/
│ ├── CrowdFundingOZ.test.ts ← Unit tests — production contract (18 tests)
│ ├── functional/
│ │ └── simulate.ts ← End-to-end business flow simulation
│ └── learning/ ← Unit tests for every learning contract
│ ├── enums/, errors/, events/
│ ├── functionModifiers/, functions/, variables/
│ └── project/ ← CrowdFunding1–6 tests
├── hardhat.config.ts
├── tsconfig.json
├── .solhint.json
├── .prettierrc
└── .env.example
The public interface that defines the protocol. Any future implementation or upgrade must implement this interface.
enum FundraisingState { Opened, Closed }
struct Contribution {
address contributor;
uint256 value;
}
function createProject(string id, string name, string description, uint256 fundraisingGoal) external;
function fundProject(uint256 projectIndex) external payable;
function changeProjectState(FundraisingState newState, uint256 projectIndex) external;
function getProjectsCount() external view returns (uint256 count);
function getContributions(string projectId) external view returns (Contribution[] memory);Events:
| Event | When |
|---|---|
ProjectCreated(string indexed projectId, ...) |
A new project is registered |
ProjectFunded(string indexed projectId, uint256 indexed value) |
A contribution is received |
ProjectStateChanged(string indexed id, FundraisingState state) |
Project state is toggled |
The production implementation. Inherits ICrowdFunding, Ownable, ReentrancyGuard, and Pausable from OpenZeppelin v5.
Custom Errors:
| Error | Condition |
|---|---|
GoalMustBeGreaterThanZero |
fundraisingGoal == 0 on createProject |
AuthorCannotFundOwnProject |
Author tries to fund their own project |
ProjectNotReceivingFunds |
Funding a Closed project |
FundValueMustBeGreaterThanZero |
msg.value == 0 on fundProject |
CallerIsNotProjectAuthor |
Non-author tries to change project state |
StateAlreadyCurrent |
Setting state to its current value |
Security design:
nonReentrantonfundProject()— prevents reentrancy attackswhenNotPausedoncreateProject()andfundProject()— emergency stop- CEI pattern (Checks → Effects → Interactions) — state is updated before the ETH transfer
Address.sendValue()instead oftransfer()— forwards all gas, safe for contract recipients- Custom errors instead of
requirestrings — 30–50% cheaper gas on revert
The contracts/learning/ directory contains the educational progression used to build the production contract. These files are preserved as-is and serve as documentation of the learning journey.
| Contract | Concept introduced |
|---|---|
variables/Identity.sol |
State variables, constructor, basic types |
functions/Sum.sol, Name.sol, Number.sol |
Function visibility, return values |
functions/Fund.sol |
Payable functions, ETH transfer |
enums/User.sol |
Enums as state representation |
errors/Asset.sol |
Custom errors with parameters |
events/Asset.sol |
Events and off-chain indexing |
functionModifiers/Permission.sol |
OZ Ownable, onlyOwner modifier |
project/CrowdFunding1.sol |
Basic constructor-based project |
project/CrowdFunding2.sol |
isAuthor / isNotAuthor modifiers |
project/CrowdFunding3.sol |
Events added to v2 |
project/CrowdFunding4.sol |
uint256 state, value and state validation |
project/CrowdFunding5.sol |
Single project stored in a struct |
project/CrowdFunding6.sol |
FundraisingState enum replaces uint |
project/CrowdFunding.sol |
Array of projects, contribution mapping |
These contracts use
pragma solidity >=0.7.0 <0.9.0and are excluded from production Solhint rules viacontracts/learning/.solhint.json.
| Tool | Version | Purpose |
|---|---|---|
| Solidity | 0.8.20 | Smart contract language — EVM target: Paris |
| OpenZeppelin Contracts | 5.6.1 | Security primitives (Ownable, ReentrancyGuard, Pausable) |
| Hardhat | 2.28.6 | Development, testing, and deployment environment |
| @nomicfoundation/hardhat-toolbox | 3.0.0 | Bundles ethers v6, TypeChain, Chai Matchers, gas reporter |
| ethers.js | 6.x (via toolbox) | Ethereum JS library — BigInt-native API |
| TypeChain | auto (via toolbox) | Generates typed TypeScript bindings from contract ABIs |
| TypeScript | 5.x strict mode | Toolchain language for all scripts and tests |
| Solhint | 6.x | Solidity linter |
| Prettier | 3.x | Code formatter (Solidity + TypeScript) |
| Node.js | 18.x or higher | Runtime requirement |
The EVM target is
parisinhardhat.config.tsto ensure compatibility across major testnets and mainnet.
Before running the project, make sure you have the following installed:
- Node.js v18 or higher — nodejs.org
- npm v9 or higher (bundled with Node.js)
Verify your versions:
node --version # must be >= 18.0.0
npm --version # must be >= 9.0.01. Clone the repository and install dependencies:
git clone https://github.com/JhonZuluaga007/FirstSmartContract.git
cd FirstSmartContract
npm installnpm install downloads all dev dependencies (Hardhat, TypeScript, Solhint, etc.), installs OpenZeppelin v5 contracts, and makes the hardhat and ts-node CLI tools available locally.
2. Compile the contracts:
npm run compileThis runs hardhat compile, which compiles all 18 Solidity contracts with solc 0.8.20, generates ABI artifacts under artifacts/, and generates TypeChain TypeScript bindings under typechain-types/.
Expected output:
Compiled 26 Solidity files successfully (evm target: paris).
3. (Optional) Configure environment variables for testnet deployment:
cp .env.example .envEdit .env with your values:
SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/YOUR_KEY
PRIVATE_KEY=0xYOUR_PRIVATE_KEY
ETHERSCAN_API_KEY=YOUR_ETHERSCAN_KEY
REPORT_GAS=true
.envis only required for testnet deployment. Local development and all tests work without it.
npm run compileRequired at least once after cloning and again after any .sol file change.
npm testRuns all 129 unit tests against Hardhat's built-in in-process blockchain. No external node required. Each test deploys a fresh contract via loadFixture.
Expected output:
129 passing (370ms)
The simulation script validates the full business flow on a single shared blockchain instance. Unlike unit tests, there are no state resets between scenarios — this validates real-world sequential interaction patterns.
npx hardhat run test/functional/simulate.tsExpected output:
=== SCENARIO 1: Happy Path ===
✅ Project alpha created
💸 Contributor 1 funded 0.5 ETH — author balance increased
💸 Contributor 2 funded 1.0 ETH — project.funds: 1.5 ETH
🔒 Fund closed project — reverts with ProjectNotReceivingFunds (expected)
✅ Project reopened by author
💸 Contributor 3 funded 0.5 ETH — goal reached
=== SCENARIO 2: Multiple Projects ===
✅ Author 1 created project-A, Author 2 created project-B
💸 Contributions isolated — getContributions() verified per project
📋 getProjectsCount() == 2
=== SCENARIO 3: Emergency Stop ===
🔒 createProject blocked while paused — EnforcedPause (expected)
🔒 fundProject blocked while paused — EnforcedPause (expected)
✅ Both operations restored after unpause
=== SCENARIO 4: Access Control ===
🔒 Non-author changeProjectState — CallerIsNotProjectAuthor (expected)
🔒 Author funds own project — AuthorCannotFundOwnProject (expected)
🔒 Non-owner pause attempt — OwnableUnauthorizedAccount (expected)
✅ Ownership transferred and verified
ALL SCENARIOS PASSED — 0 failures
You can also run the simulation against a live local node to see each transaction appear in the node's terminal output:
# Terminal 1 — start the persistent local node
npm run node
# Terminal 2 — run the simulation against it
npx hardhat run test/functional/simulate.ts --network localhostnpm run typecheckRuns tsc --noEmit to verify zero TypeScript errors across all test files and scripts without emitting output files.
# Compile all contracts and generate TypeChain types
npm run compile
# Run the full unit test suite (129 tests)
npm test
# Run only the production contract unit tests
npm run test:oz
# Run the end-to-end functional simulation (in-process, no node needed)
npx hardhat run test/functional/simulate.ts
# Run the simulation against a live local node
npx hardhat run test/functional/simulate.ts --network localhost
# Type check all TypeScript files
npm run typecheck
# Lint all Solidity contracts
npm run lint:sol
# Format Solidity contracts
npm run format:sol
# Format TypeScript files
npm run format:ts
# Start a persistent local Hardhat node (shows all transactions)
npm run node
# Deploy to local node (requires npm run node in another terminal)
npm run deploy:local
# Deploy to Sepolia testnet (requires .env configured)
npm run deploy:sepoliaThe project has 129 unit tests plus a functional simulation covering 4 end-to-end business scenarios.
npm test
# 129 passing| File | Contract | Tests |
|---|---|---|
test/CrowdFundingOZ.test.ts |
CrowdFundingOZ |
18 |
test/learning/enums/User.test.ts |
User |
3 |
test/learning/errors/Asset.test.ts |
Asset (errors) |
5 |
test/learning/events/Asset.test.ts |
Asset (events) |
5 |
test/learning/functionModifiers/Permission.test.ts |
Permission |
5 |
test/learning/functions/Fund.test.ts |
Fund |
3 |
test/learning/functions/Name.test.ts |
Asset (Name) |
2 |
test/learning/functions/Number.test.ts |
Number |
2 |
test/learning/functions/Sum.test.ts |
Sum |
5 |
test/learning/variables/Identity.test.ts |
Identity |
5 |
test/learning/project/CrowdFunding.test.ts |
CrowdFunding |
16 |
test/learning/project/CrowdFunding1.test.ts |
CrowdFunding1 |
12 |
test/learning/project/CrowdFunding2.test.ts |
CrowdFunding2 |
8 |
test/learning/project/CrowdFunding3.test.ts |
CrowdFunding3 |
7 |
test/learning/project/CrowdFunding4.test.ts |
CrowdFunding4 |
10 |
test/learning/project/CrowdFunding5.test.ts |
CrowdFunding5 |
10 |
test/learning/project/CrowdFunding6.test.ts |
CrowdFunding6 |
13 |
test/functional/simulate.ts runs sequentially on a single shared blockchain — no loadFixture resets. This validates that state transitions from one scenario carry correctly into the next, mirroring real deployment conditions.
| Scenario | What it validates |
|---|---|
| Happy Path | Create → fund (3 contributors) → close → fund attempt (reverts) → reopen → fund again → goal reached |
| Multiple Projects | Two authors, two independent projects, isolated contribution histories |
| Emergency Stop | Pause blocks all writes → unpause restores operation |
| Access Control | All 5 custom errors + OZ errors triggered, ownership transfer verified |
# Terminal 1 — start local node
npm run node
# Terminal 2 — deploy
npm run deploy:localnpm run deploy:sepoliaOutput example:
CrowdFundingOZ deployed to: 0x...
Owner: 0x...
Paused: false
| Layer | Mechanism |
|---|---|
| Reentrancy | ReentrancyGuard — nonReentrant on fundProject |
| Access control | Ownable for admin, author check for state changes |
| Emergency stop | Pausable — owner can halt all writes instantly |
| ETH transfer | Address.sendValue() — no 2300 gas limit, safe for contracts |
| State safety | CEI pattern — all state mutations before external calls |
| Gas efficiency | Custom errors — 30–50% cheaper than require strings on revert |
| Input validation | Zero-goal and zero-value checks enforced at entry points |
GPL-3.0