一、智能合约可以获得的信息
1.区块信息
智能合约的执行必须是确定性的,这也就导致了智能合约不能像通用的编程语言那样通过系统调用来得到一些环境信息,因为每个全节点的执行环境不是完全一样的,所以他只有通过一些固定的一些变量的值得到一些状态信息,下图1-1所示的表格就是智能合约能够得到的区块链的一些信息。
2.调用信息
msg.sender就是发起这个调用的人是谁,注意这个跟tx.origin(交易的发起者)是不一样的。比如说有一个外部账户A调用了一个合约叫,中有一个函数f1,f1又调用另外一个合约,里面的函数是f2。那么对于函数f2来说,msg.sender是这个合约,因为当前msg call这个调用,是这个合约发起的,而tx.origin是A这个账户,因为整个交易的发起者是A账户。
msg.gas就是当前调用还剩下多少汽油费,这个决定了我还能做哪些操作,包括你还想调用别的合约前提是还有足够的汽油费剩下来。msg.data就是所谓的叫数据域,在里面写了调用哪些函数和这些函数的参数取值。msg.sig是msg.data的前四个字节,就是函数标志符(调用的是哪个函数)。now是当前区块的时间戳,这个跟这个block.timestamp是一个意思,智能合约里没有办法获得很精确的时间,只能获得跟当前区块信息的一些时间,如图1-2所示。
3.地址类型
4.简单拍卖的例子
(1)主函数
pragma solidity ^0.4.21;
contract SimpleAuction {
address public beneficiary;//拍卖受益人
uint public auctionEnd;//结束时间
address public highestBidder;//当前的最高出价人
mapping( address => uint) bids;//所有竞拍者的出价
address[] bidders;//所有竞拍者
//需要记录的事件
event HighestBidIncreased(address bidder,uint amount);
event -Pay2Beneficiary( address - winner , uint amount);
//以受益者地址 `_beneficiary` 的名义,
//创建一个简单的拍卖,拍卖时间为 `_biddingTime` 秒。
constructor(uint _biddingTime,address _beneficiary
)public {
beneficiary = _beneficiary;
auctionEnd = now + biddingTime;
}
拍卖有一个受益人beneficiary,比如说你有一个古董要拍卖,那么这个受益人就是你;auctionEnd事整个拍卖的结束时间;highestBidder是最高出价人。拍卖的规则是这样的:在拍卖结束之前,每个人都可以去出价竞拍,竞拍的时候为了保证诚信,要把竞拍的价格的以太币发过去,(如,出价100ETH,那么你竞拍的时候要把100ETH发到这个智能合约里,它就会锁在这里面直到拍卖结束),拍卖的规则不允许中途退出,去竞拍发了100ETH,过一会儿后悔了想把钱要回来是不行的,拍卖结束的时侯出价最高的那个人highestBidder,他投出去的钱会给这个受益人beneficiary,当然你也要想办法把这个古董给最高出价人,其他没有拍卖成功的人可以把当初投进去的钱再取回来,竞拍是可以多次出价的,比如说我出个价100ETH,然后另外一个人出价110ETH,我再出价120ETH,这时只要补差价就行了(把我这一次的出价跟上一次的出价差额发到智能合约里),我上次投标的时候已经发了100ETH,这次只要再发20ETH就行了,出价要有效的话,必须比最高出价还要高,比如说当前的最高出价是100ETH,我去竞拍,我投80ETH,这个是无效的,等于是非法的拍卖。
那两个事件constructor会记录下收益人是谁,结束时间是什么时候,这个构造函数,在合约创建的时候,就记下来了。
(2)拍卖用的函数——bid函数
//对拍卖进行出价
//随交易一起发送的ether与之前已经发送的ether的和为本次出价。
function bid()public payable {
//对于能接收以太币的函数,关键字payable是必须的。
//拍卖尚未结束
require(now <= auctionEnd);
//如果出价不够高,本次出价无效,直接报错返回
require(bids[msg.sender]+msg.value > bids[highestBidder]);
//如果此人之前未出价,则加入到竞拍者列表中
if (!(bids[msg.sender] == uint(0))){
bidders.push(msg.sender);
}
//本次出价比当前最高价高,取代之
highestBidder = msg.sender;
bids[msg.sender] += msg.value;
emit HighestBidIncreased(msg.sender,bids[msg.sender]);
}
上面是拍卖用的两个函数,第一个bid函数是竞拍作用的,要竞拍就发起一个交易调用这个拍卖中合约的bid函数,这个bid函数没有参数,竞拍的时候出的价格其实是在msg.value这个地方写的,即发起调用的时候,转账转过去的以太币数目(以Wei为单位的转账金额),逻辑是:首先查一下当前的拍卖是否结束,若拍卖结束了还出价则会抛出异常;然后查一下该账户上一次的出价加上你当前发的以太币是否为最高出价(bids是个哈希表,Solidity中哈希表的特点是,如果你要查询的那个键值不存在,那么他返回默认值就是0,所以如果没有出过价,第一部分就是0);然后如果是第一次拍卖,把拍卖者的信息放到bidders数组里(原因就是Solidity哈希表不支持遍历,要遍历哈希表的话,要保存一下它包含哪些元素,然后记录一下新的最高出价人是谁,写一些日志之类的)。
(3)拍卖用的函数——拍卖结束的函数
//结束拍卖,把最高的出价发送给受益人,并把未中标的出价者的钱返还
function auctionEnd( ) public {
//拍卖已截止
require(now > auctionEnd);//该函数未被调用过
require(!ended) ;
//把最高的出价发送给受益人
beneficiary.transfer(bids[highestBidder]);
for (uint i = 0;i<bidders.length;i++){
address bidder = bidders[i];
if(bidder == highestBidder) continue;
bidder.transfer(bids[bidder]);
}
ended = true;
emit AuctionEnded(highestBidder, bids[highestBidder]);
}
首先查一下拍卖是不是已经结束了,如果拍卖还没有结束,调用这个函数就是非法的,会抛出异常;然后判断一下这个函数是不是已经被调过了,如果已经被调过了,就不用再调一遍了;如果没有,首先把这个金额给这个beneficiary,beneficiary.transfer是当前这个合约把这个金额给这个beneficiary转过去,最高出价人的钱是给受益人了;然后那些剩下的没有竞拍成功的用一个循环,把这个金额退回给这个bidder,然后标明一下,这个函数已经执行完了写一个log。
(4)上面智能合约存在的问题
写智能合约一定要小心,因为智能合约是不可篡改的,有bug也没法改。
auctionEnd这个函数必须要某个人调用才能执行,这也是Solidity语言跟其他编程语言不同的一个地方,没有办法把它设置成拍卖结束自动执行auctionEnd。可能是拍卖的受益人beneficiary去调用这个auctionEnd,也可能是参与竞拍没有成功的人去调用,总之得有一个人调用,如果两个人都调用auctionEnd,矿工在执行的时候把第一个调用执行完了,第二个再执行就执行不了了(因为第一个执行完之后,ended就是true了,没有并发执行)。
假设有一个人通过这样的一个合约账户参与竞拍,会有什么结果?
pragma solidity ^0.4.21;
import "./simpleAuctionV1.sol";
contract hackV1 {
function hack_bid(address addr) payable public {
simpleAuctionV1 sa = simpleAuctionV1(addr);
sa.bid.value(msg.value)();
}
}
这个合约实际上就一个函数hack_bid,这个函数的参数是拍卖合约的地址,然后把它转成这个拍卖合约的一个实例,然后调用拍卖合约用的bid函数,把这个钱发送过去。这实际上是一个合约账户,合约账户不能自己发起交易,所以实际上得有一个黑客从他自己的外部账户发起一个交易,调用这份合约账户的hack_bid函数,然后这个函数再去调用拍卖合约的bid函数,把他自己收到的转过来的钱(这个黑客外部账户转过来的钱再)转给这个拍卖合约中的bid函数,就参与拍卖了。
这个auctionEnd,这个合约参与拍卖没有问题,最后拍卖结束退款的时候会有什么问题?如图2-1中红框里的循环退款,退到合约账户上的钱会有什么情况,退到黑客合约账户上的钱会有什么情况?
黑客外部账户对这个合约来说是不可见的,拍卖合约能看到的只是这个黑客的合约,转账的时候没有调用任何函数,那么当一个合约账户收到转账没有调用任何函数的时候应该调用fallback函数,而这个合约没有定义fallback函数,所以会调用失败,抛出异常。transfer函数会引起连锁式的回滚,就会导致这个转账操作是失败的,会收不到钱。
再具体点,比如有20个人参与竞拍了,黑客合约是排在第10个,最高出价人排在第16个,一开始执行的时候先把钱转给受益人了(转账实际上是你这个矿工或者是全节点执行到beneficiary.transfer的时候把相应账户的余额进行了调整)。所有的Solidity语句就是智能合约执行过程中的任何对状态的修改改的都是本地的状态,都是改的本地的数据结构,所以这个合约当中无论是排在黑客合约前面还是后面,都是在改本地数据结构,只不过排在后面的bidder根本没有机会来得及执行,然后整个都回滚了,就好像这个智能合约从来没有被执行过,所以排在前面的这些转账并没有执行,就是改本地结构。如果都顺利执行完了,发布出去之后,别的矿工也把这个auctionEnd重头到尾执行一遍,也改它本地的数据结构跟你的能对得上就叫形成共识了,而不是说没有一个转账交易的语句是产生一个新的交易写到区块链上,所以都收不到钱,没有任何一个人能收到钱。
发起这个攻击的有可能是故意捣乱,写这样一个程序让大家都拿不到钱;也可能是这个人不懂,他就忘了写fallback函数了,那出现这种情况怎么办呢?如发布一个拍卖合约到区块链上,吸引很多人来拍卖,拍卖完之后发现有这样一个问题这个黑客合约,怎么办?
现在的问题是你已经把钱投进去了,锁在里面了,怎么把它取出来的问题,出现这种情况是没有办法的。有一种说法:Code is law,智能合约的规则是由代码逻辑决定的,代码一旦发布到区块链上就无法修改,即区块链的不可篡改性,好处是没有人能够篡改规则,坏处是规则中有漏洞也无法修改。智能合约设计的不好的话,有可能把以太币永久的锁起来。以前有用智能合约锁仓的,如要开发一个新的加密货币,然后pre-mining先预留一部分币给开发者,这些币都打到一个智能合约账户锁三年,三年以后这些币才能卖,这样做为了大家一开始能集中精力开发加密货币。智能合约锁仓是个常用的操作,万一要是写的时候写错了,多写一个0变成30年,这些币就会锁上30年,没有办法,类似不可撤销信托(irrevocable trust),所以在发布一个智能合约之前一定要多次测试,你可以在专门的那种测试的网上用假的以太币,做测试确认完全没有问题的情况下再发布。
如何解决?
那能不能在这个智能合约里留一个后门,用来修复bug,比如给合约的创建者超级用户的权利,在这个构造函数里加一个域叫owner,记录一下这个owner是谁,然后对这个owner的地址允许他做一些系统管理员的操作,比如可以任意转账,把钱转给哪个地址都行。如果出现像这种bug,超级管理员就可以把锁进去的钱给转出来了,这样有可能出现卷款跑路的情况。这样做的前提是所有人都要信任这个系统管理员,这个超级用户。这跟去中心化的理念是背道而驰的,也是绝大多数区块链的用户不能接受的,那怎么办呢?