Renaissance Labs

Posted on Mar 01, 2022Read on Mirror.xyz

Learn Solidity Series 10: Make Some Dapps

一、投注合约

主要功能点: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函数执行转账操作。上面代码会存在代码“重入”的风险。所以作为改善措施,应该按照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 Score[](data.length);
	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不支持uintstring类型之间的强制类型转换,因此这里我们自定义了一个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);
}

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