// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "@openzeppelin/contracts/access/Ownable.sol";
/* @title CrowdfundingWithERC20 @notice 基于ERC-20代币的众筹合约,支持项目创建、捐款、退款和取款 @dev 使用OpenZeppelin的SafeERC20防止转账错误,ReentrancyGuard防止重入 */ contract CrowdfundingWithERC20 is ReentrancyGuard, Ownable { using SafeERC20 for IERC20;
// ============ 数据结构 ============
/// @notice 项目状态枚举 enum ProjectState { Active, // 进行中 Successful,// 成功(可提款) Failed, // 失败(可退款) Cancelled // 被项目方取消 }
/// @notice 项目结构 struct Project { address creator; // 项目发起人 string title; // 标题 string description; // 描述 IERC20 token; // 募资代币(ERC-20) uint256 goal; // 目标金额(单位:token decimals) uint256 minRaise; // 最低融资额(可选0,但通常>0) uint256 deadline; // 截止时间戳(秒) uint256 totalRaised; // 已筹集总额 ProjectState state; // 当前状态 uint256 createdAt; // 创建时间 mapping(address => uint256) contributions; // 支持者 -> 捐款数额 address[] supporters; // 所有支持者列表(方便查询) }
// ============ 状态变量 ============
uint256 public projectCounter; // 项目计数器 mapping(uint256 => Project) private _projects; // 项目ID -> 项目
// ============ 事件 ============
event ProjectCreated(uint256 indexed projectId, address indexed creator, string title, uint256 goal, uint256 deadline); event ContributionMade(uint256 indexed projectId, address indexed supporter, uint256 amount); event ProjectFunded(uint256 indexed projectId, uint256 totalRaised); event ProjectFailed(uint256 indexed projectId); event ProjectCancelled(uint256 indexed projectId); event Withdrawn(uint256 indexed projectId, address indexed creator, uint256 amount); event RefundClaimed(uint256 indexed projectId, address indexed supporter, uint256 amount);
// ============ 修饰器 ============
/// @notice 检查项目是否存在 modifier projectExists(uint256 _projectId) { require(_projectId > 0 && _projectId <= projectCounter, "Project does not exist"); _; }
/// @notice 检查项目是否处于活动状态 modifier projectActive(uint256 _projectId) { require(_projects[_projectId].state == ProjectState.Active, "Project is not active"); _; }
/// @notice 仅项目创建者可调用 modifier onlyCreator(uint256 _projectId) { require(msg.sender == _projects[_projectId].creator, "Only project creator"); _; }
// ============ 构造函数 ============
constructor() { // Ownable构造函数默认设置部署者为owner,可根据需求调整 }
// ============ 核心函数 ============
/* @notice 创建新项目 @param _token 募资代币地址 @param _goal 目标金额(需与代币精度一致) @param _minRaise 最低融资额(设为0表示不设下限) @param _deadline 截止时间戳(秒) @param _title 项目标题 @param _description 项目描述 @return 新项目ID / function createProject( IERC20 _token, uint256 _goal, uint256 _minRaise, uint256 _deadline, string calldata _title, string calldata _description ) external returns (uint256) { require(_goal > 0, "Goal must be > 0"); require(_deadline > block.timestamp, "Deadline must be in future"); require(_minRaise <= _goal, "Min raise > goal"); require(bytes(_title).length > 0, "Title required");
projectCounter++; uint256 projectId = projectCounter;
Project storage p = _projects[projectId]; p.creator = msg.sender; p.title = _title; p.description = _description; p.token = _token; p.goal = _goal; p.minRaise = _minRaise; p.deadline = _deadline; p.state = ProjectState.Active; p.createdAt = block.timestamp;
emit ProjectCreated(projectId, msg.sender, _title, _goal, _deadline); return projectId; }
/* @notice 支持目标项目(捐款) @param _projectId 项目ID @param _amount 捐款金额(需大于0) */ function contribute(uint256 _projectId, uint256 _amount) external nonReentrant projectExists(_projectId) projectActive(_projectId) { Project storage p = _projects[_projectId]; require(_amount > 0, "Amount must be > 0"); require(block.timestamp < p.deadline, "Deadline passed"); require(p.state == ProjectState.Active, "Not active");
// 转移代币到合约(先转后记录,防止重入) p.token.safeTransferFrom(msg.sender, address(this), _amount);
// 更新统计 p.totalRaised += _amount; if (p.contributions[msg.sender] == 0) { p.supporters.push(msg.sender); } p.contributions[msg.sender] += _amount;
emit ContributionMade(_projectId, msg.sender, _amount);
// 检查是否已达到目标 if (p.totalRaised >= p.goal) { _setProjectState(_projectId, ProjectState.Successful); emit ProjectFunded(_projectId, p.totalRaised); } }
/* @notice 项目方取款(仅当项目成功时) @param _projectId 项目ID / function withdraw(uint256 _projectId) external nonReentrant projectExists(_projectId) onlyCreator(_projectId) { Project storage p = _projects[_projectId]; require(p.state == ProjectState.Successful, "Project not successful"); require(p.totalRaised > 0, "Nothing to withdraw");
uint256 amount = p.totalRaised; p.totalRaised = 0; // 清零防止重复提取
p.token.safeTransfer(msg.sender, amount); // 状态改为已提款?可以保留Successful或者增加一个新状态Completed,但为了简单,我们保持Successful并清除金额 emit Withdrawn(_projectId, msg.sender, amount); }
/* @notice 支持者领取退款(当项目失败时) @param _projectId 项目ID / function claimRefund(uint256 _projectId) external nonReentrant projectExists(_projectId) { Project storage p = _projects[_projectId]; require(p.state == ProjectState.Failed || p.state == ProjectState.Cancelled, "Not refundable");
uint256 contributed = p.contributions[msg.sender]; require(contributed > 0, "No contribution to refund");
// 清零记录 p.contributions[msg.sender] = 0;
p.token.safeTransfer(msg.sender, contributed); emit RefundClaimed(_projectId, msg.sender, contributed); }
/* @notice 项目方取消项目(仅当项目仍处于活跃状态) @param _projectId 项目ID / function cancelProject(uint256 _projectId) external projectExists(_projectId) onlyCreator(_projectId) projectActive(_projectId) { Project storage p = _projects[_projectId]; // 可限制在截止日期前才能取消?这里允许随时取消 _setProjectState(_projectId, ProjectState.Cancelled); emit ProjectCancelled(_projectId); }
// ============ 内部函数 ============
/* @notice 根据截止日期自动更新项目状态(检查是否失败) @dev 任何人都可以调用,触发失败处理 / function finalizeProject(uint256 _projectId) external projectExists(_projectId) { Project storage p = _projects[_projectId]; require(p.state == ProjectState.Active, "Not active"); require(block.timestamp >= p.deadline, "Deadline not reached");
if (p.totalRaised >= p.minRaise && p.totalRaised < p.goal) { // 虽未达目标但达到最低融资额?根据业务决定:此处设计为未达目标则失败 // 也可以设计为部分成功,但为了简单,我们要求必须达到goal才成功 _setProjectState(_projectId, ProjectState.Failed); emit ProjectFailed(_projectId); } else if (p.totalRaised < p.goal) { _setProjectState(_projectId, ProjectState.Failed); emit ProjectFailed(_projectId); } // 若已达目标,状态已在contribute中更新为Successful,但可能达目标后立即有人调用,会重复?需要检查状态 // 所以这里只处理Active且截止的情况 }
/// @notice 设置项目状态(仅内部调用) function _setProjectState(uint256 _projectId, ProjectState _newState) private { _projects[_projectId].state = _newState; }
// ============ 查询函数 ============
/// @notice 获取项目基本信息(返回多个值) function getProjectInfo(uint256 _projectId) external view projectExists(_projectId) returns ( address creator, string memory title, string memory description, IERC20 token, uint256 goal, uint256 minRaise, uint256 deadline, uint256 totalRaised, ProjectState state, uint256 createdAt ) { Project storage p = _projects[_projectId]; return ( p.creator, p.title, p.description, p.token, p.goal, p.minRaise, p.deadline, p.totalRaised, p.state, p.createdAt ); }
/// @notice 获取支持者捐款额度 function getContribution(uint256 _projectId, address _supporter) external view projectExists(_projectId) returns (uint256) { return _projects[_projectId].contributions[_supporter]; }
/// @notice 获取项目所有支持者地址(用于前端展示) function getSupporters(uint256 _projectId) external view projectExists(_projectId) returns (address[] memory) { return _projects[_projectId].supporters; }
/// @notice 获取项目数量 function getProjectCount() external view returns (uint256) { return projectCounter; }
// ============ 安全辅助 ============
/// @notice 允许合约接收ETH的函数(如果误转ETH,可被owner提出) receive() external payable {}
/// @notice owner提取意外转入的ETH function withdrawETH() external onlyOwner { payable(owner()).transfer(address(this).balance); } } ```
| 功能 | 实现方式 | 安全性考量 | |------|----------|------------| | 项目创建 | 使用计数器生成唯一ID,存储所有参数 | 验证deadline和goal合理性 | | 捐款 | safeTransferFrom 结合 nonReentrant | 先转账后更新状态防止重入 | | 自动成功检测 | 在contribute中检查 totalRaised >= goal 后立即变更状态 | 避免达到目标后继续捐款 | | 退款 | claimRefund 检查状态为Failed或Cancelled | 清零贡献记录防止重复退款 | | 项目方提款 | 仅在Successful状态可用,提款后清零totalRaised | 防止重复提取(状态未改但金额清零) | | 失败触发 | finalizeProject 供任何人调用,根据截止日期后余额判断 | 需检查截止时间已过 |
使用流程示例(基于Remix)
- 部署ERC-20代币(如
TestToken),给用户一些代币并授权合约 - 部署本众筹合约
- 用户A调用
createProject,传入代币地址、目标1000、截止时间等 - 其他用户调用
contribute,传入项目ID和金额,并先对众筹合约进行approve - 达到目标后状态自动变为Successful,项目方调用
withdraw取款 - 若截止日未达目标,任何用户调用
finalizeProject,状态变为Failed,支持者调用claimRefund退款
注意事项
- 代币精度:所有金额需与代币decimals一致
- Gas消耗:支持者列表
supporters数组会无限增长,建议限制项目支持者数量或使用分页查询 - 状态管理:若项目成功但creator未及时取款,合约中资金安全,但无自动过期机制
- 最低融资额:代码中
minRaise不参与自动成功判断(成功必须达goal),但可用于在finalize中判断是否部分成功?目前设计为严格达到goal才成功,minRaise仅作检查约束
此合约满足基本众筹需求,并利用OpenZeppelin库确保常见安全漏洞防护。