我们为什么要写一个众筹Dapp?
区块链的公开,透明,不可篡改,可追溯的特点。可保障投资者的利用,让项目方踏实做事
<最想解决的就是投资者不管是投资项目,还是捐款,都能大部分的掌控自己的钱以及知道自己的钱花在哪里,刚好,可以利用区块链的,公开,透明,可追溯,不可篡改的特点,来完成我们的目的>
2017年以太坊创始人V神提出了一个非常有创意的DAICO模型:
我们可以用一个简单的案例来了解一下:
项目方:发起希望图书室建设----投资人进行投资并获得相应的代币---当项目发要花投资者投的这笔钱时,需要进行请求---
单一花费单一请求----当投资者同意数过半--智能合约自动把这笔钱给项目方。
全过程公开透明,钱用在何处,转给谁都公开透明,可追溯,减少作假可能。
基于以上思想,我们开发了一个基于以太坊智能合约的Dapp应用:
1.项目方能发起众筹
2.项目方能提出花费请求
3.投资者能参与众筹
4.投资者能对参与众筹的项目进行资金支出的投票
5.投资者和项目方均能看见花费的详细信息
一.功能分析
(1)角色
项目方:发起众筹
投资方:参与众筹
(2)功能
项目方发起众筹,指定时间内众筹未达目标,则退款。
项目方能发起花费请求,花费请求必须通过参与者的投票票数决定是否执行,超过一半既可以执行。
投资方一个花费请求只能投一次票
投资方和项目方都能查看项目的完整信息。
二.合约编写
单个合约funding.sol编写
(1).一个项目需包含的信息:项目方,项目名,项目目标筹集金额,平均支持金额,项目结束时间,参与者
//1.项目发起人
//2.项目名称
//3.项目目标筹集金额
//4.每个人支持多少钱
//5.项目持续多少天
address public manager;
string public projectName;
uint256 public targetMoney;
uint256 public supportMoney;
uint256 public endTime;//终结时间
address[] investors;//所有参与方数组
SupportorFundingContract supportorFundings;//参与这个项目的人员
//构造函数
constructor(string _projectName,uint256 _targetMoney,uint256 _supportMoney,uint256 _duration,address _creator,SupportorFundingContract _supportorFundings) public{
manager = _creator;
projectName = _projectName;
targetMoney =_targetMoney;
supportMoney=_supportMoney;
endTime =block.timestamp +_duration;//block.timestamp当前时间
supportorFundings = _supportorFundings;
}
(2).单一项目需具有参与众筹(invest),查看筹集金额(getBalance),查看所有参与者(getInvestors),退款(refund)等基本方法
//使用一个mapping来判断一个地址是否是投资人,这样可以快速识别是否有投票权
mapping(address=>bool)isInvestorMap;
//投资 如果等于支付金额,则写入参与人列表
function invest() payable public{
require(msg.value == supportMoney);
investors.push(msg.sender);
isInvestorMap[msg.sender]=true;
//将投资人与当前合约的地址传递到FundingFactory中
//supportorFundings[msg.sender].push(this);
supportorFundings.setFunding(msg.sender,this);
}
//退款,由前端调用
function refund() onlyManager public {
for (uint256 i=0;i<investors.length;i++){
investors[i].transfer(supportMoney);
}
delete investors;
}
//查看当前余额
function getBalance() public view returns(uint256){
return address(this).balance;
}
//查看当前参与人
function getInvestors() public view returns(address[]){
return investors;
}
(3).项目方需进行花费请求,所以我们需要定义一个花费结构体,它应该包含(用途(purpose):买什么,金额(cost):需要花费多少,商家地址(seller):向谁购买,赞成票数(approveCount):多少参与方赞成(因为只有超过半数才能花费),花费申请状态(status):完成(已经花费),带批准(超过半数赞成),待执行(发起花费请求), 标记投过票的参与者(isVotedMap):防止重复投票)
//产品状态枚举:0:进行中,1:已批准,2:已完成
enum RequstStatus{
Voting,Approved,Completed
}
//花费请求结构
struct Request {
string purpose;//用途
uint256 cost;//花费金额
address seller;//商家地址
uint256 approveCount;//赞成票数
RequstStatus status;
//记录投资人对这个请求的投票状态,只有未投票的才能投票,每人一票
mapping(address =>bool) isVotedMap;
}
(4).一个项目可发起多个花费申请。定义一个数组(allRequests)记录所有发起的支付申请。定义发起申请的方法(createRequest)
//请求可能有多个,定义一个请求数组
Request[] public allRequests;
function createRequest(string _purpose,uint256 _cost,address _seller)onlyManager public{
Request memory req = Request({
purpose:_purpose,
cost:_cost,
seller:_seller,
approveCount:0,
status:RequstStatus.Voting
});
allRequests.push(req);//将请求放入请求数组里
}
(5).项目方发起花费申请后,需参与者进行投票,然后实现花费申请是否执行,为此我们需检查参与者是否已经投过票。定义了一个支持花费申请的函数(approveRequest)
function approveRequest(uint256 i) public {
//识别投资人是否投过票
require(isInvestorMap[msg.sender]);
//一定要使用storage类型,引用类型,否则无法修改allRequests里面的数据
Request storage req=allRequests[i];
require(req.isVotedMap[msg.sender]==false);
req.approveCount++;
req.isVotedMap[msg.sender]=true;
}
(6).当参与者完成投票后,我们就要检查花费请求是否通过,通过条件:赞成票数过半,账户余额大于花费申请金额
定义了一个完成花费请求的函数(finalizeRequest),点击完成,即完成交易和转账。
function finalizeRequest(uint256 i) onlyManager public{
Request storage req=allRequests[i];
//金额足够
require(address(this).balance >= req.cost);
//票数过半
require(req.approveCount * 2 >investors.length);
//执行转账
req.seller.transfer(req.cost);
// 更新request状态
req.status =RequstStatus.Completed;
}
(7).退款(refund),花费申请表(createRequest),完成花费(finalizeRequest)都应该是发起方才能进行的,所有我们需给这三个函数增加权限控制。定义一个修饰器,然后在方法上定义修饰器
//权限控制
modifier onlyManager{
require(msg.sender==manager);
_;
}
(8).我们还需要创建几个辅助函数,如,查看众筹项目剩余时间(getLeftTime),投资人数有多少(getInvestorsCount),
花费申请数量(getRequestsCount),某花费申请的具体信息(getRequestByIndex)
//众筹剩余时间 返回秒
function getLeftTime() public view returns(uint256){
return (endTime-block.timestamp);
}
//投资人数
function getInvestorsCount() public view returns(uint256){
return investors.length;
}
//请求数量
function getRequestsCount() public view returns(uint256) {
return allRequests.length;
}
//某花费申请具体信息
function getRequestByIndex(uint256 i) public view returns(string,uint256,address,uint256,RequstStatus){
Request memory req=allRequests[i];
return (req.purpose,req.cost,req.seller,req.approveCount,req.status);
}
合约工厂 FundingFactory.sol编写
每个人都可以创建单个众筹合约,一个人也可创建多个众筹合约,为了方便管理这些合约实例,便设置了合约工厂模式,方便管理合约。
(1).工厂合约因包含平台管理员(platformManager),平台所有的众筹合约(allFundings),自己创建合约集合(reatorFundings),自己参与过的合约集合(supportorFundings)
// 0.平台管理员
address public platformManager;
//1.所有众筹合约集合
address [] allFundings;
//2.创建人的合约集合
mapping(address => address[]) creatorFundings;
//3.参与人的合约集合
// mapping(address => address[]) supportorFundings;
SupportorFundingContract supportorFundings;
//构造函数
constructor() public{
platformManager =msg.sender;
//构造函数时创建一个全局的合约实例
supportorFundings= new SupportorFundingContract();
}
(2).合约工厂是用来管理和创建合约的,所以我们需定义创建合约的方法(createFunding)
function createFunding(string _name,uint _targetMoney,uint _supportMoney,uint _duration)public{
//创建一个合约,使用new方法,同时传入参数,返回一个地址
address funding= new Funding(_name,_targetMoney,_supportMoney,_duration,msg.sender,supportorFundings);
allFundings.push(funding);
//维护创建者所创建的合约集合
creatorFundings[msg.sender].push(funding);
}
(3).由于我们要将这些信息返回到平台上,定义几个辅助函数,返回平台所有合约(getAllFundings),返回创建者所有合约(getCreatorFundings),获取参与者所有合约(getSupportorFunding)
//返回平台所有合约
function getAllFundings() public view returns(address[]){
return allFundings;
}
//返回创建者所有合约
function getCreatorFundings() public view returns(address[]){
return creatorFundings[msg.sender];
}
//获取参与合约集合
function getSupportorFunding() public view returns(address[]){
return supportorFundings.getFundings(msg.sender);
}
由于solidity不支持在函数参数中定义复杂类型,所以我们无法直接把(支持合约的参与者集合)supportFundings变量传给invest方法。这里我们创建一个临时合约,该合约维护了所有参与者参与过的众筹合约。
SupportorFundingContract.sol
//这个合约维护全局所有参与人所参与的合约
contract SupportorFundingContract{
mapping(address=>address[])supportorFundingsMap;
function setFunding(address _supptor,address _funding){
supportorFundingsMap[_supptor].push(_funding);
}
function getFundings(address _supptor)public view returns(address[]){
return supportorFundingsMap[_supptor];
}
}
到这里,众筹的智能合约已经开发完毕了!!
然后让我们看看结果吧:
1.打开ganache-clli(模拟区块链):node_modules/.bin/ganache-cli
2.启动项目:npm run start (react开发的启动命令)
3.在浏览器中访问 http://localhost:3000/ 主界面
4.在metaMask钱包中添加账户
在ganache-cli中获取私钥:
在metamesk中添加
5.初始创世块的创建,需在代码中添加合约地址
6.现在网页就可以查看当前账户的地址了,与钱包里的地址一致(下面我们直接叫他account3(0x1b))
目前没有一个合约!!
7.使用account3作为项目方创建俩个众筹项目--刷新--合约显示
8.accounts3对《希望图书室建设》进行投资:--刷新
9.accounts2对《希望图书室建设》进行投资:在钱包切换账号--刷新--投票后,在我参与的界面可查询
10.account3在我发起的界面对“希望图书室建设”进行花费请求
11.创建花费请求成功后可在申请详情里查看
12.切换acount2账户进行投票支持
13.切换account3进行花费申请完成交易