JhonZuluaga007/crowdfunding-protocol-solidity

Decentralized crowdfunding protocol on Ethereum. OpenZeppelin v5 (Ownable · ReentrancyGuard · Pausable), ICrowdFunding interface, CEI security pattern, custom errors. 129 unit tests + end-to-end functional simulation · TypeScript · Hardhat · ethers.js v6 · TypeChain.

★ 5Forks 0TypeScriptGitHub ↗Compare
blockchain-technologycrowdfundingdefiethereumethers-jsevmhardhatopenzeppelinreentrancy-guardsecuritysmart-contractssoliditytypechainunit-testing

README

CrowdFunding Smart Contract

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.


Business Rules

The protocol allows any Ethereum account to create and fund crowdfunding projects. The following rules are enforced on-chain:

Projects

  • Any account can create a project by providing a unique id, a name, a description, and a fundraisingGoal greater than zero.
  • A project starts in the Opened state.
  • Projects are stored in an array and referenced by their index.
  • The account that creates a project becomes its author.

Funding

  • Any account except the project author can fund an Opened project 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 Closed project cannot receive funds.
  • Every contribution is recorded in the contract's contribution history, queryable by project id.

State Management

  • Only the project author can change a project's state.
  • A project can be toggled between Opened and Closed at any time by its author.
  • Setting a project to its current state is not allowed.

Emergency Stop

  • The contract owner (deployer by default) can pause() the contract at any time.
  • While paused, createProject() and fundProject() are blocked for all accounts.
  • The owner can unpause() to restore normal operation.

Ownership

  • The deployer is the initial owner.
  • Ownership can be transferred via transferOwnership().
  • Only the current owner can call admin functions (pause, unpause, transferOwnership).

Project Structure

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

Production Contract

ICrowdFunding.sol

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

CrowdFundingOZ.sol

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:

  • nonReentrant on fundProject() — prevents reentrancy attacks
  • whenNotPaused on createProject() and fundProject() — emergency stop
  • CEI pattern (Checks → Effects → Interactions) — state is updated before the ETH transfer
  • Address.sendValue() instead of transfer() — forwards all gas, safe for contract recipients
  • Custom errors instead of require strings — 30–50% cheaper gas on revert

Learning Contracts

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.0 and are excluded from production Solhint rules via contracts/learning/.solhint.json.


Tech Stack

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 paris in hardhat.config.ts to ensure compatibility across major testnets and mainnet.


Requirements

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.0

Setup

1. Clone the repository and install dependencies:

git clone https://github.com/JhonZuluaga007/FirstSmartContract.git
cd FirstSmartContract
npm install

npm 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 compile

This 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 .env

Edit .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

.env is only required for testnet deployment. Local development and all tests work without it.


Running the Project

1 — Compile

npm run compile

Required at least once after cloning and again after any .sol file change.

2 — Run the unit tests

npm test

Runs 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)

3 — Run the functional simulation

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.ts

Expected 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 localhost

4 — Type check

npm run typecheck

Runs tsc --noEmit to verify zero TypeScript errors across all test files and scripts without emitting output files.


Available Commands

# 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:sepolia

Test Suite

The project has 129 unit tests plus a functional simulation covering 4 end-to-end business scenarios.

npm test
# 129 passing

Unit Tests

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

Functional Simulation

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

Deployment

Local

# Terminal 1 — start local node
npm run node

# Terminal 2 — deploy
npm run deploy:local

Sepolia Testnet

npm run deploy:sepolia

Output example:

CrowdFundingOZ deployed to: 0x...
Owner: 0x...
Paused: false

Security

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

License

GPL-3.0

Contributors

JhonZuluaga007

Issues