下面分享几个智能合约供大家一起学习。
主要功能点:1)投注;2)开奖;3)退奖;4)获取奖池奖金;5)返回当前期数;6)返回中奖者地址;7)返回参与彩民的地址;
合约主要包含有四个属性:
contract Lottery {
address manager; // 管理员
address[] players; // 投了注的彩民
address winner; // 上期彩票的胜出者
uint256 round = 1; // 第几期
}
管理员属性可以在创建合约的时候进行初始化。
constructor() public {
manager = msg.sender;
}
假设每次投注只能投1个以太币。
// 投注
function play() public payable {
require(msg.value == 1 ether);
players.push(msg.sender);
}
开奖就是在投注彩民数组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++;
}
只有管理员才可以发起退奖操作。
// 退奖
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;
// 获取奖金池的金额
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)获取花费申请的详情;
第一步:定义合约。
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));
}
接着在FundingFactory中添加其他操作合约的方法。
// 获取所有众筹合约
function getAllFundings() public view returns(address[] memory) {
return allFundings;
}
// 获取发起者的众筹合约
function getCreatorFundings() public view returns(address[] memory) {
return creatorFundings[msg.sender];
}
第一步:为了便于维护参与者的众筹记录,我们定义一个合约,专门用来记录所有参与者参与过的众筹合约。
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);
}
第一步:在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));
}
// 获取已经筹集到的金额,即当前合约的余额
function getBalance() public view returns(uint) {
return address(this).balance;
}
// 获取所有的参与者
function getInvestors() public view returns(address payable[] memory) {
return investors;
}
如果合约还没结束,允许投资人退出众筹。所以我们在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;
}
如果众筹成功,管理员在使用众筹资金前,需要发起花费。
第一步:在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);
}
审核通过条件: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;
}
}
结束花费申请的条件: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;
}
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不支持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)管理投标合约资金的发放;
发布商品者(卖方):发布商品;
投标者或出价者(买方):进行投标和揭标操作;
仲裁者:负责确定中标者;
// 竞拍合约
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;
}
}
// 发布商品
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;
}
// 通过商品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);
}
投标必须要满足以下条件:
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;
}
竞标结束后,由买方公告价格。最后以买方公告价格的最高者作为赢家。
// 揭标(买方公告价格)
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;
}
仲裁者可以确定最终的中标方。
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;
}
上面代码创建了一个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);
}
}
}
// 获取竞标赢家信息
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);
}
上面就是竞标合约的所有代码介绍。