# Learn Solidity Series 10: Make Some Dapps

By [bit1](https://paragraph.com/@bit1) · 2022-03-18

---

一、投注合约
------

主要功能点：1）投注；2）开奖；3）退奖；4）获取奖池奖金；5）返回当前期数；6）返回中奖者地址；7）返回参与彩民的地址；

1.1 定义合约属性
----------

合约主要包含有四个属性：

    contract Lottery {
        address manager;  // 管理员
        address[] players;  // 投了注的彩民
        address winner;   // 上期彩票的胜出者
        uint256 round = 1;    // 第几期
    }
    

管理员属性可以在创建合约的时候进行初始化。

    constructor() public {
        manager = msg.sender;
    }
    

1.2 投注
------

假设每次投注只能投1个以太币。

    // 投注
    function play() public payable {
        require(msg.value == 1 ether);
        players.push(msg.sender);
    }
    

1.3 开奖
------

开奖就是在投注彩民数组`players`中随机选出一个彩民，然后将合约的余额转账到该彩民的地址上。

这里需要先定义一个修饰器，用于限定只有管理员角色有权执行开奖的方法。

    modifier onlyManager() {
       require(manager == msg.sender);
        _;
    }
    

下面是开奖方法的实现逻辑：

    function kaijiang() public payable {
        // 生成players数组的随机下标
        bytes memory v1 = abi.encodePacked(block.difficulty, now, players.length);
        bytes32 v2 = keccak256(v1);
        uint v3 = uint256(v2) % players.length;
        // 获取中奖者
        winner = players[v3];
        // 把奖池的金额转账给中奖者
        winner.transfer(address(this).balance);
        // 清空plays
        delete players;
        // 期数加1
        round++;
    }
    

1.4 退奖
------

只有管理员才可以发起退奖操作。

    // 退奖
    function tuiJiang() public onlyManager {
        require(players.length != 0);
        // 把奖池的金额退还给每一个玩家
        for (uint i = 0; i < players.length; i++) {
            players[i].transfer(1 ether);
        }
        // 清空plays
        delete players;
        // 期数加1
        round++;
    }
    

> 值得注意的是，上面开奖和退奖方法中，都会执行`transfer`函数执行转账操作。上面代码会存在代码“[重入](https://docs.soliditylang.org/en/develop/security-considerations.html#re-entrancy)”的风险。所以作为改善措施，应该按照`Checks-Effects-Interactions`模式编写函数代码。

优化后的代码：

    function kaijiang() public payable {
        // 生成随机下标
        bytes memory v1 = abi.encodePacked(block.difficulty, now, players.length);
        bytes32 v2 = keccak256(v1);
        uint v3 = uint256(v2) % players.length;
        // 获取中奖者
        winner = players[v3];    
        // 清空plays
        delete players;
        // 期数加1
        round++;
        // 把奖池的金额转账给中奖者
        winner.transfer(address(this).balance);
    }
    
    // 退奖
    function tuiJiang() public onlyManager {
        require(players.length != 0);
        // 清空plays
        delete players;
        // 期数加1
        round++;
        // 把奖池的金额退还给每一个玩家
        for (uint i = 0; i < players.length; i++) {
            players[i].transfer(1 ether);
        }
    }
    

上面代码将transfer操作放在方法最后执行。另外，上面代码通过delete players删除所有参与的玩家信息，这样会存在一定的风险：如果数组较大，可能会超过区块gas限制，从而引发out of gas异常。其实对于动态数组而言，使用length属性修改数组大小也可以达到同样效果。

    players.lenght = 0;
    

1.5 其他操作
--------

    // 获取奖金池的金额
    function getAmount() public view returns(uint256) {
        return address(this).balance;
    }
    
    // 获取管理员地址
    function getManagerAddress() public view returns(address) {
        return manager;
    }
    
    // 返回当前期数
    function getRound() public view returns(uint256) {
        return round;
    }
    
    // 返回中奖者地址
    function getWinner() public view returns(address) {
        return winner;
    }
    
    // 返回参与彩民的地址
    function getPlays() public view returns(address[]) {
        return players;
    }
    

二、众筹合约\\

主要功能点：1）创建众筹合约；2）获取所有众筹合约；3）获取发起者的众筹合约；4）获取参与者的众筹合约；5）参与众筹；6）获取已筹集到的金额；7）获取所有参与者；8）退款；9）众筹成功后，发起花费请求；10）审批花费请求；11）完成花费请求；12）获取众筹的剩余时间；13）获取参与者的数量；14）获取花费申请的详情；

2.1 创建众筹合约

第一步：定义合约。

    contract Funding {
        // 合约发起者
        address public manager;
        // 众筹项目名称
        string public projectName;
        // 目标金额
        uint public targetMoney;
        // 支持金额
        uint public supportMoney;
        // 项目结束时间，单位秒
        uint public endTime;
        // 参与者
        address payable[] investors;
        
        constructor(string memory _projectName, uint _targetMoney, uint _supportMoney, uint _duration, address _creator) public {
            manager = _creator;
            projectName = _projectName;
            targetMoney = _targetMoney;
            supportMoney = _supportMoney;
            endTime = now + _duration;
        }
    }
    

第二步：定义一个工厂合约，该合约保存了所有的众筹实例，并提供了操作众筹合约的方法；

    
    contract FundingFactory {
        // 平台管理员
        address public platformManager;
        // 所有的众筹合约
        address[] allFundings;
        // 自己创建的合约集合，key代表合约发起者地址，value代表合约机制的集合
        mapping(address => address[]) creatorFundings;
     
        constructor() public {
            platformManager = msg.sender;
        }
    }
    

第三步：定义发起众筹的方法；

    function createFunding(string memory _projectName, uint _targetMoney, uint _supportMoney, uint _duration) public {
        Funding funding = new Funding(_projectName, _targetMoney, _supoortMoney, _duration, msg.sender);
        allFundings.push(address(funding));
        creatorFundings[msg.sender].push(address(funding));
    }
    

2.2 获取所有众筹合约和发起者的众筹合约
---------------------

接着在FundingFactory中添加其他操作合约的方法。

    // 获取所有众筹合约
    function getAllFundings() public view returns(address[] memory) {
       return allFundings;
    }
    
    // 获取发起者的众筹合约
    function getCreatorFundings() public view returns(address[] memory) {
        return creatorFundings[msg.sender];
    }
    

2.3 获取参与者参与过的众筹集合
-----------------

第一步：为了便于维护参与者的众筹记录，我们定义一个合约，专门用来记录所有参与者参与过的众筹合约。

    contract SupportFunding {
        // 记录参与过的众筹
        mapping(address => address[]) supportFundings;
        
        function setSupportFunding(address _supportor, address funding) public {
            supportFundings[_supportor].push(funding);
        }
    
        function getSupportFunding(address _supportor) public {
        return supportFundings[_supportor];
        }
    }
    

第二步：在`FundingFactory`中增加supportFunding属性，并提供获取参与者参与过的合约方法；

    
    SupportFunding supportFunding;
    
    // 获取参与者参与过的众筹合约
    function getSupportFunding() public view return(address[] memory) {
        return supportFunding.getFundings(msg.sender);	
    }
    

2.4 参与众筹
--------

第一步：在`Funding`合约增加一个`supportFunding`属性，在该合约的构造函数中对该属性执行初始化；

    SupportFunding supportFunding;
    
    constructor(string memory _projectName, uint _targetMoney, uint _supportMoney, uint _duration, address _creator, SupportFunding _supportFunding) public {
            manager = _creator;
            projectName = _projectName;
            targetMoney = _targetMoney;
            supportMoney = _supportMoney;
            endTime = now + _duration;
            supportFunding = _supportFunding;
        }
    }
    

第二步：定义参与众筹方法；

    // 记录是否投资人，key代表参与者，value代表是否投资人
    mapping(address => bool) isInvestors;
    
    // 参与众筹
    function invest() public payable returns(uint) {  
        // 约束条件：发送币的数量必须要等于支持金额
        require(msg.value == supportMoney);
        // 记录投资人
        investors.push(msg.sender);
        // 记录参与者为投资人
        isInvestors[msg.sender] = true;
        // 记录参与者参与过的合约（当前合约）
        supportFunding.setFunding(msg.sender, address(this));
    }
    

2.5 获取已筹集到的金额和所有的参与者
--------------------

    
    // 获取已经筹集到的金额，即当前合约的余额
    function getBalance() public view returns(uint) {
        return address(this).balance;
    }
    
    // 获取所有的参与者
    function getInvestors() public view returns(address payable[] memory) {
        return investors;
    }
    

2.6 退款
------

如果合约还没结束，允许投资人退出众筹。所以我们在`Funding`合约中定义一个修饰器，该修饰器用于添加方法的约束条件。

    modifier onlyManager {
        require(msg.sender == manager);
    }
    

然后定义退款方法，并使用上面定义好的修饰器。

    // 退款
    function refund() onlyManager public {
        // 循环遍历所有投资人，并向其转账
        for (uint i = 0; i < investors.length; i++) {
        investors[i].transfer(supportMoney);
        }
        // 清空投资人数组
        delete investors;
    }
    

2.7 申请花费
--------

如果众筹成功，管理员在使用众筹资金前，需要发起花费。

第一步：在`Funding`合约中定义一个结构体，用于记录花费的详情。

    // 花费申请状态，0代表申请中，1代表已批准，2代表已完成
    enum RequestStatus {
        Voting, Approved, Completed
    }
    
    // 记录花费申请信息
    struct Request {
        // 花费目的
        string purpose;
        // 申请花费的金额
        uint cost;
        // 转账给商家的地址
        address payable seller;
        // 花费申请赞同的票数
        uint approveCount;
        // 申请状态
        RequestStatus status;
        // 参与者的投票状态，true代表已经投票，false代表还没投票
        mapping(address => bool) hasVoted;
    }
    

第二步：在`Funding`合约中增加一个属性，该属性用于记录管理员发起的花费请求。

    
    Request[] requests
    

第三步：定义申请花费的方法；

    // 发起花费申请
    function createRequest(string memory _purpose, uint _cost, address payable _seller) onlyManager public {
        Request memory req = Request({
        purpose: _purpose,
        cost: _cose * 10**18,  // 将花费金额转换成单位wei
        seller: _seller,
        approveCount: 0,
        status: RequestStatus.Voting
        });
        requests.push(req);
    }
    

2.8 发起申请审核
----------

审核通过条件：1）消息发起者是投资人；2）投资人之前没有投过票；

    // 对指定索引的申请进行审核
    // 参数i代表数组的索引
    function approveRequest(uint i) public {
        Request storage req = requests[i];
        // 检查是否是投资人
        require(isInvestors[msg.sender] == true);
        // 检查是否投过票
        require(req.hasVoted[msg.sender] == false);
        // 赞同数增加
        req.approveCount++;
        // 记录该投资人已经投过票
        req.hasVoted[msg.sender] = true;
        // 如果票数过半，更新申请状态为Approved
        if (req.approveCount * 2 > investors.length) {
        req.status = RequestStatus.Approved;
        }
    }
    

2.9 完成花费
--------

结束花费申请的条件：1）合约余额必须要大于等于申请的花费金额；2）赞同票数要过半；

    // 完成花费
    function finalizeRequest(uint i) onlyManager public {
        Request storage req = requests[i];
        // 合约的余额必须要足够支付花费
        require(address(this).balance >= req.cost);
        // 申请状态必须为已通过
        require(req.status == RequestStatus.Approved);
        // 向商家转账
        req.seller.transfer(req.cose);
        // 更新申请状态为已完成
        req.status = RequestStatus.Completed;
    }
    

2.10 获取众筹的剩余时间、参与者的数量、花费申请的详情
-----------------------------

    
    function getLeftTime() public view returns(uint) {
        return endTime - now;
    }
    
    function getRequestCount() public view returns(uint) {
        return requests.length;
    }
    
    function getRequest(uint i) public view returns(string memory, uint, address, uint, RequestStatus) { 
        Request storage req = requests[i];
        return(req.purpose, req.cost, req.seller, req.approveCount, req.status);
    }
    

到这里为止，整个众筹合约的核心代码已经完成。

三、智能评分合约
--------

该合约实现了根据每一个学员的计算分数指标m和n计算出他们的最终成绩。

首先在合约中定义一个结构体，用于封装学员相关的信息。

    contract SmartScore {
        
        struct Score {
            // 学生ID
            uint uid; 
            // 课程ID
            uint cid; 
            // m为被其他学生发现并成功评价的实验操作错误数量
            uint m; 
            // n为成功评价其他学生实验操作错误数量
            uint n; 
            // 分数
            uint score; 
        }
        
    }
    

定义一个计算学员成绩的函数。函数入参是一个数组类型，而数组中每一个元素也是一个长度为`4`的一维数组，分别用于存储学员ID，课程ID，分数指标m，分数指标n、以及最终成绩。

    // 设置完成后，计算学生成绩
    function calc(uint[4][] memory data) public pure returns(string memory) {
        Score[] memory scores = new ScoreUnsupported embed;
        uint i = 0;
        for (i = 0; i < data.length; i++) {
            uint uid = data[i][0];
            uint cid = data[i][1];
            uint m = data[i][2];
            uint n = data[i][3];
            Score memory score = Score(uid, cid, m, n, 0);
            scores[i] = score;
        }
        // 找出最高分数，以n-m的值作为参考值
        Score memory maxScore = scores[0];
        for (i = 1; i < scores.length; i++) {
            int a1 = int(maxScore.n - maxScore.m);
            int a2 = int(scores[i].n - scores[i].m);
            if(a2 > a1) {
                maxScore = scores[i];
            }
        }
        // 计算alpha参数的公式为：100 = 80 + (n - m) * alpha
        uint alpha = 20 / (maxScore.n - maxScore.m);
        // 计算每个学生的成绩
        for (i = 0; i < scores.length; i++ ){
            scores[i].score = 80 + (scores[i].n - scores[i].m) * alpha;
        }
        // 返回学员的成绩，返回格式: uid_cid_score#uid_cid_score#uid_cid_score#...
        string memory result = "";
        for (i = 0; i < scores.length; i++ ){
            Score memory sc = scores[i];
            result = result.concat(uint2str(sc.uid).concat("_").concat(uint2str(sc.cid)).concat("_").concat(uint2str(sc.score)));
            if (i != scores.length - 1) {
                result = result.concat("#");
            }
        }
        return result;
    }
    

因为solidity不支持`uint`和`string`类型之间的强制类型转换，因此这里我们自定义了一个`uint2str`函数，用于将一个`uint`类型变量转换成`string`类型变量。

`uint2str`函数的实现如下：

    function uint2str(uint i) internal pure returns (string memory c) {
        if (i == 0) {
            return "0";
        }
        uint j = i;
        uint length;
        while (j != 0){
            length++;
            j /= 10;
        }
        bytes memory b = new bytes(length);
        uint k = length - 1;
        while (i != 0){
            b[k--] = byte(uint8(48 + i % 10));
            i /= 10;
        }
        return string(b);
    }
    

上面代码就是将`uint`类型的变量中每一个数字转换成对应的`ascii`码后，再封装到`bytes`里面，最终再将`bytes`转换成`string`类型。

另外，合约里面还需要实现字符串的拼接。遗憾的是，solidity并没有提供字符串拼接的工具。所以需要我们自己来实现字符串的拼接。

    library StringUtils {
        
        function concat(string memory self, string memory s) internal pure returns (string memory) {
            bytes memory b = new bytes(_a.length + _b.length);
            bytes memory _a = bytes(self);
            bytes memory _b = bytes(s);
            uint k = 0;
            uint i = 0;
            for (i = 0; i < _a.length; i++) {
                b[k++] = _a[i];
            }
            for (i = 0; i < _b.length; i++) {
                b[k++] = _b[i];
            }
            return string(b);
        }
    
    }
    

这里我们定义了一个库合约，用于提供字符串拼接的函数。然后在`SmartScore`合约中通过`using..for`语法将库合约引入进来。

    
    using StringUtils for string;
    

四、拍卖合约
------

主要功能点：1）卖方发布商品；2）读取商品信息；3）投标；4）揭标；5）仲裁者确定中标者；6）获取赢家信息；7）获取参与竞标的人数；8）管理投标合约资金的发放；

> 发布商品者（卖方）：发布商品；

投标者或出价者（买方）：进行投标和揭标操作；\\

仲裁者：负责确定中标者；

4.1 定义合约
--------

    // 竞拍合约
    contract Auction {
        // 用于统计竞标商品数量，作为ID
        uint public productIndex;
        // 该mapping存储了商品Id与竞标获得者地址的对应关系
        mapping(uint => address payable) productIdInStore;
        // 该mapping存储了竞标获得者参与过的拍卖商品之间的关系
        mapping(address => mapping(uint => Product)) stores;	
        // 竞标商品的状态，0代表开始拍卖，1代表交易成功，2代表交易不成功
        enum ProductStatus {
            Open, Sold, Unsold
        }
        // 竞标商品的使用状态，0代表未使用过，1代表使用过
        enum ProductCondition {
            New,  Used
        }
        // 竞标人信息
        struct Bid {
            // 投标人
            address bidder;
            // 竞标商品ID
            uint productId;
            // 虚拟投标价格
            uint value;
            //是否已经揭标
            bool revealed; 
        }
        // 商品信息
        struct Product {
            // 商品id
            uint id;    
            // 商品名称             
            string name;    
            // 商品类别         
            string category ;   
            // 图片Hash  
            string imageLink ;
            // 图片描述信息的Hash       
            string descLink;   
            // 竞标开始时间     
            uint auctionStartTime; 
            // 竞标结束时间
            uint auctionEndTime;  
            // 竞标初始价格  
            uint startPrice;       
            // 出价最高者
            address payable highestBidder; 
            // 赢家得标的价格
            uint highestBid ; 
            // 竞标价格第二名      
            uint secondHighestBid ; 
            // 竞标总人数
            uint totalBids ;   
            // 竞标商品的状态     
            ProductStatus status;
            // 竞标商品的新旧标识
            ProductCondition condition;
            // 存储所有竞标人的信息
            mapping(address => mapping(bytes => Bid)) bids;
        }
    }    
    

4.2 发布商品
--------

    // 发布商品
    function addProductToStore(string memory _name, string memory _category, string memory _imageLink, string memory _descLink, uint _auctionStartTime, uint _auctionEndTime ,uint _startPrice, uint _productCondition) public  {
        // 开始时间需要小于结束时间
        require(_auctionStartTime < _auctionEndTime, "开始时间不能晚于结束时间");
        // 商品ID自增
        productIndex += 1;
        // 创建Product实例
        Product memory product = Product(productIndex,_name,_category,_imageLink,_descLink,_auctionStartTime,_auctionEndTime,_startPrice,address(0x0),0,0,0,ProductStatus.Open,ProductCondition(_productCondition));
        // 保存商品
        stores[msg.sender][productIndex] = product;
        productIdInStore[productIndex] = msg.sender;
    }
    

4.3 读取商品信息
----------

    // 通过商品ID读取商品信息
    function getProduct(uint _productId)  public view returns (uint,string memory, string memory,string memory,string memory,uint ,uint,uint, ProductStatus, ProductCondition)  {
        Product memory product = stores[productIdInStore[_productId]][_productId];
        return (product.id, product.name,product.category,product.imageLink,product.descLink,product.auctionStartTime,product.auctionEndTime,product.startPrice,product.status,product.condition);
    }
    

4.4 投标
------

投标必须要满足以下条件：\\

1）必须在竞拍时间内；\\ 2）投标价格必须大于等于商品的拍卖价格；\\ 3）投标人没有该商品的投标记录；

    // 投标, 其中_bid参数代表投标人的ID，由外部生成传入
    function bid(uint _productId, bytes memory _bid) payable public returns (bool) {
        Product storage product = stores[productIdInStore[_productId]][_productId];
        require(now >= product.auctionStartTime, "商品竞拍时间未到，暂未开始，请等待...");
        require(now <= product.auctionEndTime,"商品竞拍已经结束");
        require(msg.value >= product.startPrice,"设置的虚拟价格不能低于开标价格");
        require(product.bids[msg.sender][_bid].bidder == address (0x0), "bidder的值必须为空"); 
        // 设置投标人
        product.bids[msg.sender][_bid] = Bid(msg.sender, _productId, msg.value, false);
        // 投标人数递增
        product.totalBids += 1;
        // 返回投标成功
        return true;
    }
    

4.5 揭标
------

竞标结束后，由买方公告价格。最后以买方公告价格的最高者作为赢家。

    // 揭标（买方公告价格）
    function revealBid(uint _productId, string memory _amount, bytes memory _bid) public {
        // 通过商品ID获取商品信息
        Product storage product = stores[productIdInStore[_productId]][_productId];
        // 检查条件：投标必须要结束了
        require(now > product.auctionEndTime,"竞标尚未结束，未到公告价格时间");
        // 获取投标人信息
        Bid memory bidInfo = product.bids[msg.sender][_bid];
        // 检查条件：投标人不为空
        require(bidInfo.bidder > address (0x0), "钱包地址不存在");
        // 检查条件：必须是未揭标状态
        require(bidInfo.revealed == false, "已经揭标");
        // 退款金额
        uint refund;
        // 公告价格
        uint amount = stringToUint(_amount);
        // 如果买方设置的虚拟价格低于公告价格，则执行退款操作
        if (bidInfo.value < amount) {
            refund = bidInfo.value;
        } else {
            // 如果第一个参与公告价格者，那么更新商品竞标信息，并将虚拟价格与实际竞标价格之间的差额退还给买方
            if (address(product.highestBidder) == address (0x0)) {
                // 设置当前出价者为最高价的竞拍者
                product.highestBidder = msg.sender;
                // 将公告价格作为最高价格
                product.highestBid = amount;
                // 将商品的起始拍卖价格作为第二高价格
                product.secondHighestBid = product.startPrice;
                // 将虚拟价格与公告价格之间的差额作为退款
                refund = bidInfo.value - amount;
            } else { 
                // 如果参与者不是第一个揭标，那么分为三种情况：
                // 第一种情况：实际竞标价大于商品的最高竞标价；
                // 第二种情况：实际竞标价小于商品的最高竞标价，但是大于第二高竞标价；
                // 第三种情况：实际竞标价小于第二高竞标价；
                if (amount > product.highestBid) {
                    // 将原来的最高价竞拍者修改为第二高价竞拍者
                    product.secondHighestBid = product.highestBid;
                    // 将原来最高竞拍价退还给最高价竞拍者
                    product.highestBidder.transfer(product.highestBid);
                    // 将当前出价者作为最高价竞拍者
                    product.highestBidder = msg.sender;
                    // 将当前出价作为最高价
                    product.highestBid = amount;
                    // 将虚拟价格与公告价格之间的差额作为退款
                    refund = bidInfo.value - amount;
                } else if (amount > product.secondHighestBid) {
                    product.secondHighestBid = amount;
                    // 退还所有竞标款
                    refund = amount;
                } else { 
                    // 如果出价比第二高价还低的话，直接退还竞标款
                    refund = amount;
                }
            }
            // 更新状态为已揭标
            product.bids[msg.sender][_bid].revealed = true;
            // 退款
            if (refund > 0){ 
                msg.sender.transfer(refund);
            }
        }
    }
    

下面是`stringToUint`函数的实现：

    function stringToUint(string memory s) pure private returns (uint) {
        bytes memory b = bytes(s);
        uint result = 0 ;
        for (uint i = 0; i < b.length; i++ ){
            if (uint(uint8(b[i])) >= 48 && uint(uint8(b[i])) <= 57){
                result = result * 10  + (uint(uint8(b[i])) - 48);
            }
        }
        return result;
    }
    

4.6 确定中标者
---------

仲裁者可以确定最终的中标方。

    function finalizeAuction(uint _productId) public {
        Product memory product = stores[productIdInStore[_productId]][_productId];
        require(now > product.auctionEndTime, "当前时间必须大于竞拍结束时间");
        require(product.status == ProductStatus.Open, "竞拍状态必须是开始状态");
        require(product.highestBidder != msg.sender, "仲裁者不能够是最高出价者");
        require(productIdInStore[_productId] != msg.sender, "仲裁者不能够是卖方");
        // 如果竞拍人数为0，则无需做任何操作
        if (product.totalBids == 0) {
            product.status = ProductStatus.Unsold;
        } else {
            // 创建托管合约实例
            Escrow escrow = (new Escrow).value(product.secondHighestBid)(_productId, product.highestBidder, productIdInStore[_productId], msg.sender);
            // 竞拍合约包含了托管合约的引用
            productEscrow[_productId] = address(escrow);
            // 更新状态
            product.status = ProductStatus.Sold;
            // 中标方只需要支付第二高投标价格，差额会退还给中标方
            uint refund = product.highestBid - product.secondHighestBid;
            // 退还差额
            product.highestBidder.transfer(refund);
        }
        // 更新商品信息
        stores[productIdInStore[_productId]][_productId] = product;
    }
    

4.7 管理资金发放
----------

上面代码创建了一个`Escrow`合约实例，该合约主要负责管理合约资金的发放。

    contract Escrow {
        // 商品ID
        uint public productId;
        // 买房（中标者）
        address payable public buyer;
        // 卖方
        address payable public seller;    
        // 仲裁者
        address public arbiter;
        // 投标金额
        uint public amount;
        // 是否已经将投标金额支付给卖方或买方
        bool public isDisbursed;
        // 是否同意支付给卖方
        mapping(address => bool) isReleased;
        // 同意支付人数
        uint public releaseCount;
        // 是否同意退还给买方
        mapping(address => bool) isRefunded;
        // 同意退还人数
        uint public refundCount;
    
        event CreateEscrow(uint _productId, address _buyer, address _seller, address _arbiter);
        event UnlockAmount(uint _productId, string _operation, address _operator);
        event DisburseAmount(uint _productId, uint _amount, address _beneficiary);
    
        constructor(uint _productId, address _buyer, address _seller, address _arbiter) payable public {
            productId = _productId;
            buyer = _buyer;
            seller = _seller;
            arbiter = _arbiter;
            amount = msg.value;
            isRefunded= false;
            emit CreateEscrow(_productId, _buyer, _seller, _arbiter);
        }
      
        // 获取Escrow合约的详情
        function getEscrowInfo() public view returns(address, address, address, bool, uint, uint) {
        return(buyer, seller, arbiter, isDisbursed, releaseCount, refundCount);
        } 
    
        // 只要有两位任意买方、卖方或仲裁者同意，那么就会向卖方发放投标金额
        function releaseToSeller(address caller) public {
            require(!isDisbursed, "支付状态必须是未支付");
            if ((caller == buyer || caller == seller || caller == arbiter) && !isReleased[caller]) {
            isReleased[caller] = true;
            releaseCount += 1;
            emit UnlockAmount(productId, "release", caller);
        }
        if (releaseCount == 2) {
            seller.transfer(amount);
            isDisbursed = true;
            emit DisburseAmount(productId, amount, seller);
        }
        }
    
        // 只要有两位任意买方、卖方或仲裁者同意，那么就会向买方退还投标金额
        function refundToBuyer(address caller) public {
            require(!isDisbursed, "支付状态必须是未支付");
            if ((caller == buyer || caller == seller || caller == arbiter) && !isFunded[caller]) {
            isRefunded[caller] = true;
                 refundCount += 1;
                 emit UnlockAmount(productId, "refund", caller); 
        }
        if (releaseCount == 2) {
             buyer.transfer(amount);
             isDisbursed = true;
             emit DisburseAmount(productId, amount, buyer);
         }
        }
    }
    

4.8 获取竞标赢家信息
------------

    // 获取竞标赢家信息
    function getWinnerInfo(uint _productId) public view returns (address, uint ,uint) {
        Product memory product = stores[productIdInStore[_productId]][_productId];
        return (product.highestBidder, product.highestBid, product.secondHighestBid);
    }
    
    // 获取竞拍人数
    function getTotalBids(uint _productId) public view returns(uint) {
        Product memory product = stores[productIdInStore[_productId]][_productId];
        return (product.highestBidder, product.highestBid, product.secondHighestBid);
    }
    

上面就是竞标合约的所有代码介绍。

---

*Originally published on [bit1](https://paragraph.com/@bit1/learn-solidity-series-10-make-some-dapps)*
