0%

ETH九---智能合约

智能合约

​ 智能合约是比特币和以太坊的一个重要的区别,智能合约的本质是运行在区块链上的一个代码,代码的逻辑定义智能合约的内容,智能合约的账户里保存了合约当前的运行状态。

​ 上图是智能合约的代码结构,第一行首先声明所使用的solidity的版本号,不同版本的solidity语言在语法上有一些小的差别,contract类似于java中的类,address类型是solidity语言钟所特有的地址类型的变量。

接下来来个事件,是用来记录日志的,代码的例子是网上拍卖的例子,如果出现某个出现了一个新的最高价,我们就记录一下这个参数。接下来的事件叫Pay2Beneficiary,参数是赢得拍卖的这个人的地址,以及他最后的出价,solidity语言和其他的语言有很多特别之处,比如这个’mapping(address => uint) bids’,这是一个哈希表,保存了从地址到uint的一个映射,solidity语言中的哈希表中的一个奇怪的地方是,它不支持遍历,这里使用bidders数组来记录哈希表中的数据,以便于遍历。

然后就是三个成员函数,都是public的,说明其他账户都可以调用这些函数,bid函数上的标识payable后面会解释。

​ 外部账户如何调用智能合约?

如果A->B,B如果是普通账户,那就和比特币中的转账没有什么区别,如果B是一个合约账户,那么这个转账实际上就发起了对B合约的调用,具体调用的是合约中的哪个函数,是在data域中决定的。

sender address是发起调用的账户的地址,to contract address是被调用的合约的地址,调用的函数就是tx data(左下角),如果这个函数是由参数的话,参数的取值,也是在这个data域中说明的。

中间一行是调用合约的参数:

value:发起调用时,转过去多少钱,这里是0(这个调用的目的仅仅是为了调用这个函数,不是真的要转账),gas used是这个交易花了多少汽油费,gas price是单位汽油的价格,gas limit是这个交易我最多愿意支付的汽油。

​ 一个合约如果调用另外一个合约函数?

LogCallFoo方法只是写log;

callAFooDirectly这个函数,是传给他一个A的地址,在其中创建A的实例,并调用foo方法。

以太坊中规定,一个交易只有外部交易才能够发起,合约账户不能自己主动发起一个交易,所以这个例子当中需要有一个外部账户,调用合约B中的函数,这个函数在调用A当中的foo函数。

这个调用和上一个调用相比的区别是:错误处理的不同,上种方法,如果你调用的哪个合约在执行过程中出现错误,会导致发起调用的这个合约,也跟着一起回滚(A抛出异常,B也会跟着出错),而这种方法,在调用过程中,被调用的合约抛出异常,call函数会返回false,表明调用失败,但是发起调用的函数,并不会抛出异常,可以继续执行。

delegatecall不需要切换到被调用的合约的环境中执行,而是在当前调用的合约的环境中执行,比如就用当前账户的账户余额,存储之类的。


这里的payable,只有第一个函数中有,其他两个没有,解释:

以太坊中规定,如果这个合约账户要能接收外部转账的话,那么必须标注成payable,bid函数的功能:使用来进行竞拍,出价的,比如你要参与拍卖,你说你出100个以太币,那么你就调用这个合约中的这个bid函数,所以拍卖的规则是,你调用这个bid函数时,要把你拍卖的出价(100个以太币)也发送过去,存储在这个合约里,锁定在那一直到拍卖结束,避免有的人凭空出价,所以bid函数要有能力接受外部转账的能力。

withdraw函数是拍卖结束了,出价最高的那个人赢得了拍卖,其他人没有得到想要的东西,可以调用withdraw函数,把自己当初出的价钱(锁定在以太坊中的以太币取回来),因为不涉及对智能合约的转账,而仅仅是通过withdraw,取回锁定的钱。

**注意:**以太坊中凡是要接受外部转账的函数,都要标注为payable,否则的话,如果你给这个函数转出去钱的话,回引出错误处理(抛出异常).如果你不需要接受外部转账,这个函数就不用写上payable。


上图中的函数既没有参数也没有返回值,而且也没有函数名(匿名函数)。

A调用B合约的时候,然后要在交易中的data域说明你调用的是B合约中的哪个函数,如果A给合约B转了一笔钱,没有说明调用的是哪个函数,怎么办?(data域是空的)这时候缺省的就是调用fallback函数(没有什么函数好调了,所以就调它),还有一种情况就是你调用的函数不存在,也是调用的这个fallback函数,这就是为什么这个函数没有参数和返回值。(这个fallback函数一般情况下都是写成payable)。

​ 我们说转账金额可以是0,但是汽油费是必须给的,这是两码事,转账金额是给收款人的,汽油费是给发布这个区块的矿工的。(汽油费不给的话,矿工不会把你这个交易打包发布到区块链上)。

智能合约的创建和运行

​ 智能合约是由某一个外部账户发起一个转账交易,转给0x0这个地址,然后把要发布的合约的代码,放在data域里。

EVM和JVM是类似的思想,通过加一层虚拟机,对智能合约的运行提供一个一致性的平台,所以EVM有时候叫做world wide computer,EVM的寻址空间非常大,有256位,我们的个人电脑一般是64位的,和这个没法比。

​ 如果比较以太坊和比特币两种区块链的编程模型,他们的设计理念有很大的差别,比特币的设计理念是简单,脚本语言的功能很有限,比如说不支持循环,而以太坊是要提供一个图灵完备的编程模型,很多功能在比特币平台实现起来很困难,甚至是根本实现不了,而到以太坊平台实现起来就很容易,当然,这样也出现一个问题,出现死循环怎么办?当一个全节点,收到一个对智能合约的调用,怎么知道这个调用执行起来会不会导致死循环?

没有办法。这实际上是个‘停机问题’(Halting Problem),‘停机问题’是不可解的,这个问题不是NPC的(NPC问题是可解的),只不过他没有多项式时间的解法,很多NPC问题,有很自然的指数时间的解法。比如哈密顿回路,他是可能有解的,只不过这个解的复杂度可能是指数级的,而Halting Problem是不可解的,从理论上可以证明,不存在这样一个算法,能够对任意给定的输入程序判断出是否会停机。

有没有哈密顿回路,如果你不考虑复杂度的话,这个其实是很好解的:把所有的可能性枚举一遍,比如说你有n个顶点,n个顶点的排列有n!,把每个组合检查一下,是不是构成合法回路,就知道有没有哈密顿回路,只不过这个解的复杂度是指数级的。

​ 担心出现死循环怎么办?

把这个问题推给发起交易的哪个账户,你发起一个对智能合约的调用,你要支付相应的汽油费

上图中的AccountNonce就是交易的序号,用于防止前面所说的replay attack;

Price和GasLimit和汽油费相关的,GasLimit是这个交易愿意支付的最大汽油量,Price是单位汽油的价格,乘积就是这个交易可能支付的最大汽油费;

Recipient是收款人的地址;

Amount:转账金额,把Amount的金额装给Recipient,所以我们可以看到,交易中的汽油费和转账金额是分开的;

Payload:前面说的data域,用于存放调用的是合约中的哪一个函数,参数之类

​ 当一个全节点收到一个对智能合约的调用的时候,先按照这个调用中给出的GasLimit,算出可能花掉的最大汽油费,然后一次性地把汽油费从发起调用的这个账户上扣掉,然后再根据实际执行的情况,算出实际花了多少汽油费,多退少补(其实如果汽油费不够的话,会引起回滚,就会一次性把这个汽油费全退回),一些简单的指令,比如加法、减法,消耗的汽油费是很少的,复杂的指令如取哈希,消耗的汽油费就很多,尽管这个运算一条指令就可以运行,除了计算量之外,需要存储状态的指令,消耗的汽油费也是比较大的,相比之下,如果你仅仅是为了读取公共数据,那些指令可以是免费的。

错误处理

​ 以太坊中的交易执行起来具有原子性,一个交易要么全部执行,要么全不执行,不会只执行一部分。这个交易既包含转账交易,也包含对智能合约的调用,所以如果在执行智能合约调用的过程当中,出现任何错误,会导致整个交易的执行回滚,退回到开始执行之前的状态,就好像这个交易从来没有执行过。

​ 出现错误的情况:

①汽油费,如果这个交易执行完之后,没有达到当初的GasLimit,那么多余的汽油费会被退回到这个账户里,相反的,如果执行到一半,GasLimit已经都用完了,这个合约的执行要退回到开始执行之前的状态,这就是一种错误处理,而且这时候已经消耗的汽油费是不退的,要不然的话,就会有恶意结点可能会发动Denial-of-service attack,可能他发布一个计算量很大的合约,然后不停的调用,每次调用的时候给的汽油费都不够,反正汽油费还会退回来,这对恶意节点没有什么损失,但对矿工来说,是白白浪费很多资源。

require通常用来处理外部的错误,比如说判断函数的输入是否符合要求,这里的例子是一个bid函数,判断当前时间now是否是小于等于拍卖结束的时间,如果不符合,这个时候就会抛出异常。

revert是无条件会抛出异常,如果你执行到revert语句,他自动就会回滚,早期的版本里他用的是throw语句。

​ 智能合约出现错误会导致回滚,那么如果是嵌套调用,一个智能合约调用另外一个智能合约,那么被调用的哪个智能合约出现错误,是不是会导致发起调用的智能合约也一起回滚呢?

不一定,取决于调用智能合约的方式,如果这个智能合约,直接调用的话,那么会触发连锁式的回滚,整个交易都会回滚,如果调用的方式是call方式,就不会引起连锁式回滚,只会使当前的调用失败,返回一个false的返回值,有些情况下,从表面上看,你并没有调用任何一个函数,比如只是往一个账户里转账,但是如果这个账户是一个合约账户的话,转账的本身就有可能触发对函数的调用。(因为会调用fallback函数)


区块的块头中也有GasLimit,发布区块需要消耗一定的资源,那么我们要不要对这个区块发布消耗的资源有一个限制,比特币当中对发布的区块也是有一个限制的(大小,最多不能超过1M),以太坊中这么规定是不行的,智能合约的调用是非常复杂的,有的交易可能从字节数上来看可能是很小的,但是消耗的资源可能很大(可能调用别的合约之类的),所以要根据这个交易的具体操作来收费(这就是汽油费),GasLimit是这个区块里所有的交易能消耗汽油的一个上限(这个不是说把这个区块中所有交易的GasLimit加在一起,如果这样的话,就没有限制了)。

每个矿工可以对这个GasLimit进行微调,可以再上一个区块的GasLimit基础上上调或者下调1/1024,1/1024看起来很小,以太坊的出块速度很快,所以如果大家都觉得现在的GasLimit太小,那么很快就会翻一番,下调一样,所以这种机制下求出的GasLimit,是所有矿工认为比较合理的GasLimit的平均值。

​ 假设某个全节点要打包一些交易到区块链里面,这些交易里有一些是对智能合约的调用,那么这个全节点是应该先把这些智能合约都执行完之后,再去挖矿呢?还是应该先挖矿,获得记账权,然后再去执行智能合约?

区块链里有一笔转账交易发布上去的话,本来就是需要全节点都去执行的,这个不是浪费,也不是一种问题,所有的全节点都要同步状态,在本地执行交易,如果要有一个全节点不执行,就会出问题,它的状态和别的全节点不同步,比特币也是一样。

但是执行智能合约,就要从发起交易的账户收取汽油费,那么如果有多个人都执行这个智能合约,会把这个汽油费收很多份。

我们说全节点收到一个对合约的调用的时候,要一次性的先把这个调用可能花掉的最大汽油费,从发起这个调用的账户上扣掉,这个具体是怎么操作的?

Receipt数据结构

​ 状态树,交易树,收据树三棵树都是全节点在本地维护的数据结构,状态树记录了每个账户的状态,包括账户余额,所以扣除汽油费是怎么扣的?全节点收到这个调用之后,从我本地的数据结构里把他账户里的余额减掉就行了,如果余额不够的话,这个交易就不能执行。执行完了如果有剩的,再把它的余额再加回来,多个人执行带智能合约的调用,只是在本地将要扣除的汽油费扣除而已。任何状态的修改都是修改本地的数据结构,只有在合约执行完了,发布到区块链上之后,在本地的修改才会变成外部可见的,才会变成共识。如果某个全节点发布了一个区块,我收到这个区块之后,在本地执行的代码就扔掉了,再按照区块里的交易再执行一遍,更新本地的三棵树。

​ 以太坊挖矿,也是尝试nonce,计算哈希的时候要用到什么?

用到block header的内容,Root,TXHash,ReceiptHash是三棵树的根哈希值,所以应该先执行完这个区块中的所有交易,包括智能合约的交易,这样才能够更新这三棵树,这样才能知道这三棵树的根哈希值,block header的内容才能确定,然后才能尝试各个nonce。

​ 假设我是一个矿工,费了半天劲执行这些智能合约,消耗了我本地的好多资源,最后我挖矿没有挖到怎么办?能得到什么补偿

首先汽油费是没有的,因为汽油费是给获得记账权发布区块的哪个矿工。

①因为有ghost协议,所以我可以继续挖矿,当一个叔父区块,一般不这么干,如果都这么干,共识机制就比较难达成了,等于你故意造成分叉,这么干对自己也没有什么好处,挖到叔父区块还是得切换到最长合法链上,所以代价没有省下来,为什么不先切换到最长合法链上继续挖呢?挖矿是无记忆的,前面挖了多少和后面是没有关系的,不如切换到最长合法链,然后继续挖。

以太坊中没有任何补偿,不仅如此,还要把别人发布的区块里的交易在本地执行一遍,以太坊中规定要验证发布区块的正确性,每个全节点要独立验证。

​ 所以,这种机制下,挖矿慢的矿工,就比较吃亏,本来汽油费的设置是出于对矿工执行这些智能合约消耗的资源得一种补偿的目的,但是这种补偿,只有挖到矿的矿工才能得到,其他的矿工相当于“陪挖”。

​ 会不会有的矿工,觉得不给我汽油费,那我就不验证了?

按照协议,我要验证一下获得记账权的矿工发布区块的正确性,我验证他能得什么好处?又不给汽油费,我认为你是正确的不就行了吗,接着往下挖,会不会有矿工“想不开”?

出现这种情况会导致什么后果?最直接的后果是,危害区块链的安全,区块链的安全要求所有的全结点要独立验证发布的区块的合法性,这样少数有恶意的结点才没有办法篡改区块链上的内容,如果某个矿工想不通,不给钱就不验证了,那么这样的风气蔓延开,就会危害区块链的安全。

如果他跳过验证的这个步骤,就没法在挖矿了,因为验证的时候是把交易都执行一遍,更新本地的三棵树,如果不验证的话,三棵树的内容,没有办法更新,以后就没有办法发布新的区块了,本地的状态就不对了,算出的根哈希值别人认为是错的。(这三棵树的内容是不会出现的发布的区块中的,只有在本地验证之后才有),这就对应了前面所说的,不能把状态树的全部内容都发布到区块链上,太多了而且很多是重复的

​ 矿池的有些做法是,很多矿工合在一起,矿工本身不验证,矿池中有个pool manager全节点是负责统一验证,矿工就都相信这个全节点验证完之后的正确性。

​ 发布到区块链上的交易是不是都成功执行的?如果智能合约执行过程中出现了错误,要不要也发布到区块链上去?

执行发生错误的交易,也要发布到区块链上去,否则的话,汽油费扣不掉,光是在本地的数据结构上扣除汽油费是没用的,拿不到钱,你得把他发布到区块链上之后,形成共识,扣掉的汽油费才能形成你账户上的钱,所以发布到区块链上的交易不一定都是成功执行的,要告诉大家为什么扣汽油费,别人也要验证一遍,把这个交易执行一遍,看你这个交易执行的是不是对的

​ 怎么知道一个交易是不是执行成功了呢?

我们前面说的那三棵树,每个交易执行完之后,形成一个交易,Status域就是交易执行的情况

​ 智能合约是不是支持多线程?多核处理器很普遍,智能合约支不支持多核并行处理?

solidity不支持多线程,他根本没有支持多线程的语句,以太坊是一个交易驱动的状态机,必须是完全确定性的,给定一个智能合约,面对同一组输入,产生的输出,或者转移到下一个状态必须是完全确定的,所有的全节点都得执行同一组操作,到达同一个状态,如果状态不确定,三棵树的根哈希值完全对不上,必须完全确定才行,多线程的问题在于:多个核对内存访问顺序不同的话,执行结果有可能是不确定的。http://zhenxiao.com/papers

​ 除了多线程之外,造成执行结果不确定的操作,也都不支持。

产生随机数,这个操作必须是不确定性的,如果不同的机器产生的随机数都一样,那不叫随机数了,所以以太坊中的智能合约没有办法产生真正意义上的随机数,可以用一些伪随机数,不能是真的随机数,否则的话,会出现前面的问题。每个全节点执行的结果都不一致。

​ 智能合约的执行必须是确定性的,这就导致了智能合约不能像通用的编程语言那样通过系统调用得到system call的一些信息(环境信息),每个全节点执行的环境不是完全一样的,所以只有通过一些固定的一些变量的值,能够得到一些状态信息。

上图中的msg.sender消息发送者和下面tx.origin交易发起者不是一个东西,有个外部账户A调用了一个合约C1,C1中有个函数f1,函数f1调用了另外一个合约账户C2里面的函数f2,对于函数f2来说msg.sender是C1合约,因为当前这个msg call是C1这个合约发起的;tx.origin是A账户,整个交易最初的发起者是A账户。

msg.gas指的是当前交易还剩下多少汽油费,决定了我还能够做什么操作,包括像调用别的合约,前提是还有别的汽油费剩下来。

msg.data就是所谓的数据域,在里面写了调用了哪个函数,和这个函数所使用到的参数取值。

now和上一个图中的timestamp是一个意思,智能合约里没有精确的时间,只能获得和当前区块相关的时间

balance是一个成员变量,其他的都是成员函数,uint256是成员变量的类型,不是函数调用(不是参数)。addr.balance是这个地址上这个账户的余额,addr.transfer(12345)感觉是addr这个账户向外转了12345个wei,但是问题来了,这个函数只有一个参数,没有说转给谁,所以addr.transfer指的是当前这个合约往addr这个地址里转入多少钱,addr是转入的地址,不是转出的地址,那么哪个是转入的地址?C这个智能合约里面有个函数f,f包含addr.transfer,意思是从C这个账上,往这个addr账户中转入xx钱,其他的函数也类似(比如addr.call,是当前合约,调用addr这个合约,而不是addr这个账户调用别的账户)。

transfer和send都是用来转账的函数,区别是transfer会导致连锁性回滚,而send不会导致连锁回滚,call其实也可以转账,addr.call.value(amount)。call也不会引起连锁式回滚,另外一个区别是transfer和send只给了2300的汽油费(这是非常少的),转账的合约基本上干不了其他的事,只是写个log的费用,而call是把当前这个调用剩下的所有汽油都发过去,比如说call所在的这个合约本身被调用的时候还剩8000个汽油,然后去调别的合约的时候用call转账,就把8000个汽油都转过去了。call之后,如果当前合约还有别的操作,call调用的别的合约,如果有剩余的汽油,会返回,然后可以再执行相应的其他操作。

例子

比如说你有一个古董要拍卖,拍卖的受益人就是你 。

在拍卖结束之前,每个人都可以去出价竞拍,竞拍的时候为了保证诚信,要把你竞拍的价格的相应的以太币发过去,比如说你出价100个以太币,竞拍的时候把这100个以太币发到智能合约里,而且会锁在这里面,知道竞拍结束,这个拍卖的规则不允许中途退出。拍卖结束出价最高的人他投出去的钱会给受益人,当然,你应该把古董给最高出价人,那么其他没有竞拍成功的人,可以把当初投进去的钱取回来,(也就是withdraw函数)。竞拍可以多次出价。出价有效的话,要比当前最高出价还要高。

bid函数没有参数,感觉好像你出价的时候不需要告诉对方你出价多少?他在里面有个msg.value:这是发起这个调用的时候转账转过去的以太币的数目,以wei为单位的转账金额,这个逻辑是,查一下当前拍卖还没结束,如果结束了,还出价会抛出异常,然后查一下你上次出价,加上当前这个调用发出去的以太币,大于最高出价,如果你以前没有出价过?solidity中的哈希表,如果键值不存在,那么返回的默认值就是0,之后将拍卖者的信息放到bid数组里,之后记录一下最高价

拍卖结束的函数,查一下拍卖是否已经结束了,如果有人调用,是非法的,会抛出异常,require(!ended)判断一下当前函数是否已经被调用过了,如果已经被调用过了,就不用再调用。beneficiary.transfer指的是当前这个合约,把bids[highestBidder]金额给beneficiary转过去,对于没有竞拍成功的人,进行循环,把这个金额退回给这个bidder。然后标注一下ended=true,表示这个函数已经被执行了。

​ 有什么问题么?

如果你写完一个拍卖程序,然后你要先把他发布到区块链上:往0地址发一笔转账交易,转账的金额是0,但是汽油费是要交的,把这个智能合约的代码放在data域里面,矿工将这个智能合约发布到区块链上之后,会返回给你一个合约的地址,然后这个合约就在区块链上了,所有人都可以调用它。

智能合约本身有一个合约账户,里面有它的状态信息。拍卖的时候要调用bid函数,当前账户调用的bid函数正是矿工写在区块链里的。

写智能合约一定要小心,因为智能合约是不可篡改的,说得不好听点就是有bug没有办法改。

​ auctionEnd函数必须要某个人调用才能执行,这个也是solidity语言和其他语言不同的地方,就是没有办法把它设置成拍卖结束了自动执行auctionEnd,可能是拍卖的受益人调用,也可能是某个参与竞拍没有成功的人,总之得有一个人去调用,如果两个人都去调用auctionEnd,矿工在执行的时候,把第一个调用执行完了,然后第二个调用就执行不了了(没有并发执行!)

这个合约实际上就一个函数,参数就是拍卖合约的地址,把它转成拍卖合约的一个实例,然后调用拍卖合约中的bid函数,把这个钱发送过去,这是一个合约账户,合约账户不能自己发起交易,所以实际上是有一个黑客,从他自己的外部账户,发起一个交易,调用这个合约账户的hack_bid函数,然后hack_bid函数再去调用拍卖合约中的bid函数,把他收到的自己外部账户转过来的钱,再转给拍卖合约中的bid函数,参与拍卖,看似没有什么问题;但是在退款的时候,在一个合约账户收到转账,没有调用任何函数的时候的时候,应该调用fallback函数,而hackV1没有定义fallback函数,所以会调用失败,抛出异常,transfer函数会引起连锁式地回滚,所以导致转账操作是失败的,收不到钱,对黑客有什么好处?

有可能是故意捣乱,也有可能是这个人不懂,忘了写fallback函数。

退款只是改本地的数据结构,没有形成交易,如果都执行完了,发布出去之后,别的矿工也把这个auctionEnd从头到尾执行一遍,也改他本地地数据结构,这就叫形成共识,不管排在黑客前面的拍卖者还是后面的拍卖者,只不过后面的bidder根本不能执行,然后整个都回滚了,就好像这个智能合约没有被执行过一样,排在前面的对bidder转账并没有执行,所以都收不到钱。

​ 出现上面的情况怎么办?你已经把钱投在里面了,怎么把钱拿出来的问题

没有办法。Code is law,智能合约的规则是由代码逻辑决定的,而代码一旦发布到区块链上,就改不了了,就是所谓的区块链的不可篡改性,这样的好处是,没有人可以篡改规则,坏处是,规则中有漏洞,你也改不了了,智能合约如果设计的不好的话,有可能把收到的以太币永久的锁起来,谁也取不出来。以前还有人用智能合约锁仓的,就比如要开发一个新的加密货币,像前面的pre mining,预留一些币给开发者,然后这些币都打在一个智能合约账户上,锁仓三年,三年之后,这些币才能卖,这样做才能为了让大家在一开始的时候都集中精力开发这个加密货币,智能合约锁仓是个常用操作,万一要是写错了,多写一个0,这些币就会锁上三十年,这个有点像irrevocable trust(不可撤销的信托),美国有一些有钱人用这种信托以达到财产保护,或者是减税的目的,就是一个法律上的合同,如果制定这种不可撤销的信托的时候,法律条款有问题,也可能会导致存进去的钱取不出来。所以在你发布一个智能合约之前,一定要测试!

​ 我能不能在智能合约里留个后门?给合约的创建者一个超级用户的权力?

在智能合约的构造函数中加一个owner,这个owner可以做一些系统管理员的操作,可以任意转账,把钱转给哪个账户都行,出现上面的bug,owner就可以起到作用。

这样做的前提是,所有人都要新人owner,这个和去中心化的理念是背道而驰的,也是绝大多数区块链的用户不能接受的


​ 第二版:由投标者自己取回出价

这里就不用循环了,每个竞拍没有成功的人,自己调用withdraw函数,把钱取回来,先判断是不是最高出价者,如果是的话,不能把钱给他,因为要留着给受益人,再判断这个人的账户余额是不是正的,amount是账户余额,将账户余额转给sender,然后将bids清零,免得下次再来取钱

​ 这样可以了么?

hack_bid和前面的hack_bid相同

hack_withdraw在拍卖结束的时候把钱取回来,这样看上去好像没问题,问题在于fallback函数又把钱取了一遍,fallback中的msg.sender是拍卖合约,拍卖合约又会给他转一次钱,账户清零的操作只有在转账成功之后才会执行,但是现在已经陷入到了和黑客合约中的递归调用当中,根本执行不到,所以最后的结果是,黑客不停地从拍卖合约中取钱,第一次取得是自己的出价,后面取得就是别人的钱了。

这个递归取钱持续到什么时候结束?

①拍卖合约上的余额不够了,不足以在支持转账的语句

②汽油费不够了

③调用栈溢出了

所以黑客合约会先判断,之后在进行攻击。

​ 解决办法:可以先清零,再转账。

在区块链上,任何未知的合约,都可能是有恶意的,所以每次你向对方转账,或者是调用对方的某个函数,都要提醒一下自己,这个合约这个函数,有可能反过来调用你当前这个合约,并且修改状态,除了上述方法,还有下面的一种解决方法:

首先将清零的位置提前了,而且转账的时候用的是send,send和transfer的一个共同特点是转账的时候发送的汽油费只有2300个单位,这个不足以让接收的合约再发起一个新的调用,只够写一个log而已。