前言
当下,开发以太坊智能合约使用最多的语言是solidity。这门语言最早由以太坊创世团队的Gavin Hood设计,后由以太坊技术团队开发维护。其特性结合了JavaScript、Java、Python等的特点。随着版本更迭,如今正走在更安全更严谨的道路上。
学习智能合约开发,可以从官方文档开始。戳这里:solidity官方文档。文档的第三章《Solidity by Example》提供了几个很不错的案例,这里我挑有意思的分析注解一下。
背景
第一个合约案例是《Simple Open Auction》,它的功能是模拟现实中最常见的拍卖形式。过程很简单:
购买者(Bidder)调用合约对拍卖品出价,出价会以Ether转账的形式暂存给合约。合约要求在一定时间段内完成竞价。竞价结束后合约统计最高出价者,退还除最高出价者之外的其他人暂存的钱款。并将最高价对应的钱款转给出售者(Beneficiary)。
代码
pragma solidity >=0.4.22 <0.6.0;
contract SimpleAuction {
// 受益人,出售者
address payable public beneficiary;
// 竞价结束时间
uint public auctionEndTime;
// 最高价出价者
address public highestBidder;
// 最高价
uint public highestBid;
// 允许撤回之前的出价
// 该map记录每个竞价者的最高出价
mapping(address => uint) pendingReturns;
// 竞价结束标识位
bool ended;
// 事件一:最高价发生变化时
event HighestBidIncreased(address bidder, uint amount);
// 事件二:竞价结束时
event AuctionEnded(address winner, uint amount);
// (0.5.2新格式)-->特殊注解<--
// 出现时机:当用户被要求确认交易时就会弹出注释内容
/// 构造函数
// 竞价持续时间 `_biddingTime`
/// 受益人账户地址 `_beneficiary`.
constructor( uint _biddingTime,address payable _beneficiary) public {
beneficiary = _beneficiary;
auctionEndTime = now + _biddingTime;
}
/// 竞价函数直接由调用者发起,价格有msg.value确定
/// 如果竞价失败,value中的钱可以在结束后撤回
function bid() public payable {
// 关键字payable用来标明合约函数可以接受Ether币.
require(
now <= auctionEndTime,
"Auction already ended."
);
// 如果价格不超过最高价,钱会被退回.
require(
msg.value > highestBid,
"There already is a higher bid."
);
if (highestBid != 0) {
// 安全编写须知!!!
// 这一时刻旧的最高价要被退回出价者
// 直接调用highestBidder.send(highestBid)会有安全风险,因为send()接受特殊构参数后可以调用任意合约。
pendingReturns[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
emit HighestBidIncreased(msg.sender, msg.value);
}
/// 撤回一个被超过的价格.
function withdraw() public returns (bool) {
uint amount = pendingReturns[msg.sender];
if (amount > 0) {
// 安全编写须知!!!
// 一定要先将map中的值清零,因为接收者可以在send()函数返回前利用接受合约重新调用withdraw()函数
// 从而发生“多转”的bug,即重放攻击!!!
pendingReturns[msg.sender] = 0;
if (!msg.sender.send(amount)) {
// No need to call throw here, just reset the amount owing
pendingReturns[msg.sender] = amount;
return false;
}
}
return true;
}
function auctionEnd() public {
// 在于其他合约交互时(i.e. 调用合约函数或转账) 可以分成3个阶段:
// 1. 检查条件;
// 2. 执行动作; (potentially changing conditions)
// 3. 与其他合约交互;
// 安全设计!!!
// 如果这三个阶段混到一块了,其他合约就可能调回当前合约从而修改状态变量。
// 这会导致一些过程(i.e. 以太币转账)被执行多次。
// 如果内部函数有和外部合约交互的过程,那么也该遵循以上准则。
// 1. Conditions
require(now >= auctionEndTime, "Auction not yet ended.");
require(!ended, "auctionEnd has already been called.");
// 2. Effects
ended = true;
emit AuctionEnded(highestBidder, highestBid);
// 3. Interaction
beneficiary.transfer(highestBid);
}
}
总结
这个官方案例过程并不复杂,值得思考的点是注解中的安全编码建议。例如,里面提到的重放攻击就曾发生在THE DAO合约中,造成了非常严重的损失。现在的智能合约机制还不是特别完善,存在不少有隐患的安全问题。开发者是该多总结之前血淋淋的教训。