ETH-智能合约(区块链技术与应⽤)
智能合约是⽐特币和以太坊最⼤的区别。
什么是智能合约
智能合约是运⾏在区块链上的⼀段代码,代码的逻辑定义了智能合约的内容。
智能合约的账户⾥保存了合约的当前的运⾏状态,包含:
Balance 当前余额
nonce 交易次数
coding 合约代码
storage 存储,存储的数据结构是⼀棵MPT
智能合约的代码⼀般是⽤solidity语⾔来编写的,语法和接近。
solidity
address是solidity特有的,后⾯会详细讲到。
mapping是从地址到⽆符号整数的映射,
event 是⽤来记录⽇志的,这个例⼦中第⼀个event的参数是拍卖的地址和⾦额,第⼆个是获胜者的地址和拍卖⾦额
solidity 不⽀持遍历,如果想要遍历元素,⾃⼰需要想办法记录⼀下哈希表有哪些元素,这⾥是⽤bidders数组记录的,solidity语⾔中数组可以是固定长度也可以是动态改变长度的。
构造函数有两种,第⼀种是像C++构造函数⼀样,定义⼀个与contract同名的函数,函数可以有参数但是不能有返回值;新版本更推荐的是⽤constructor来定义构造函数,这个函数只有合约在创造的时候才被调⽤⼀次,构造函数也只有⼀个。
成员函数⾥第⼀个有payable,另外两个函数没有,因为以太坊规定,合约账户要能接受外部转账的话必须标注为payable。
eg:下图这是⼀个⽹上拍卖的合约,这个例⼦中的bid函数是⽤来竞拍出价的,⽐如说要参与拍卖,要出100个以太币,那么就调⽤合约中的bid函数。所以拍卖的规则是调⽤bid函数的时候要把拍卖的出价即100个以太币也发送过去存储在合约⾥,锁定那⾥⼀直到拍卖结束。避免有⼈凭空出价(实际上没有那
么多钱,漫天喊价),所以拍卖的时候要把出的价钱发在智能合约⾥锁定起来。所以这个bid函数要有能够接受外部转账的能⼒,所以才标注了payable。
成员函数的withdraw函数就没有payable,withdrew函数的⽤处是在拍卖结束时,出价最⾼的⼈赢得拍卖,其他⼈没有拍到想要拍到的东西,可以调⽤withdraw函数,把⾃⼰当初的出价,也就是之前bid的时候锁定在智能合约⾥的以太币,再取出来。withdraw的⽬的不是真的转账,不需要把钱转给智能合约,⽽仅仅是通过调⽤withdraw函数把当初锁定在智能合约的钱取回来,所以没必要⽤payable。
如何调⽤智能合约
外部账户调⽤
调⽤智能合约和转账类似,A->B转账,如果B是个普通账户,那么这只是⼀个普通的转账交易,和BTC的转账交易是⼀样的,如果B是合约账户的话,那么这个转账其实是发起⼀次对B的合约的调⽤,具体调⽤的是合约中的哪个函数是在数据域(data域)中另外说明的。
send address是发起调⽤的账户地址,To Contract Address是被调⽤的合约的地址,调⽤的函数是TXdata⾥⾯给出的要调⽤函数,如果这个函数有参数,那么其参数也在这⾥的data域⾥说明的,上⾯的
案例的三个成员函数都是没有参数的,但是有⼀些成员函数是有参数的。
中间⼀⾏是调⽤的参数,Value是发起调⽤的时候转账花的钱数,这⾥是0,说明这⾥只是想调⽤函数并不想真的转账,所以这⾥的To contract address函数不需要定义payable。Gas used是这个交易所花的汽油费,gas priced 是单位汽油的价格,gas limit是这⽐交易愿意⽀付的最多汽油。
合约账户调⽤
直接调⽤
A合约就只是写成Log(⽇志),event是定义⼀个事件,叫LogCallFoo,emit 来调⽤这个事件,emit的作⽤就是写⼀个log,对于程序运⾏没有影响;B合约中函数的参数是⼀个地址,就是A合约的地址,然后这个语句把这个地址转换成A这个合约的⼀个实例,然后调⽤foo这个函数。
以太坊中规定⼀个交易只有外部账户才能发起,合约账户不能⾃⼰主动发起⼀个交易。这个例⼦当中实际上是需要⼀个外部账户调⽤了合约B当中的函数CallAFooDirectly,然后这个函数再调⽤A合约中的foo函数。
使⽤address中的call⽅法
第⼀个参数是函数的signature,后⾯跟的是调⽤参数。
这两种⽅式的区别在于:对于错误处理的不同。
直接调⽤中被调⽤的那个合约执⾏中如果出现错误,那么会导致调⽤的那个合约⼀起发⽣回滚,例如图中A出现异常,那么会导致B跟着⼀起抛出异常;
⽽对于第⼆种⽅式,如果被调⽤的合约出现了异常,那么调⽤的合约中call函数只会返回⼀个false,表明这个调⽤时失败的,发起调⽤的函数并不会抛出异常,⽽是可以继续执⾏。delegatecall()
与call()的⽅法基本上是⼀样的,⼀个主要的区别是,Delegatecall()不需要切换到被调⽤的合约的环境中去执⾏,⽽是在当前的环境中执⾏就可以了。⽐如就⽤当前的账户余额存储之类的。
以太坊中凡是要接受外部转账的函数都需要标志为payable,否则的话你给这个函数转钱就引发错误处理抛出异常,如果你不需要接受外部转账,函数就不⽤写payable。具体如上的代码
fallback函数
⽆参数⽆返回值,⽆函数名,fallback关键字并没有出现在函数名⾥⾯。
调⽤合约的时候,A调⽤B合约,要在转账交易的data域说明调⽤的是合约B中的哪个函数,如果A给B转了⼀笔钱,没有说明调⽤的是哪个函数,也就是data域是空的,这个时候缺省的就是调⽤这个fallback函数,这也是为什么叫fallback函数,因为没有别的函数可以调⽤了,就只能调⽤他。还有⼀种情况是你要调⽤的函数不存在,在你的data域⾥你说你要调⽤这个函数,实际合约当中没有这个函数,也是调⽤fallback函数,这也是为什么这个函数没有参数也没有返回值。
fallback函数也可能需要标注payable关键词,就如果fallback函数需要有接受转账的能⼒的话是需要写payable,⼀般情况都是写成payable,如果合约账户没有任何函数标志为payable,包括fallback函数也没有,那么这个合约没有任何能⼒可以接受外部的转账。如果有⼈往合约⾥转钱就会引发异常。
转账⾦额可以为0,是给收款⼈的,但是汽油费是要给矿⼯的,不给的话矿⼯不会把交易打包到区块链上的。
智能合约的创建
智能合约的创建是由某⼀个外部的账户发起⼀笔转账交易,转给0X0地址,然后把要发布的合约代码放到data域⾥⾯。
智能合约运⾏在EVM上。Java Virtual Machine(JVM)是为了增强可⼀致性,EVM也是类似的思想,通过加⼀层虚拟机,对智能合约的运⾏提供⼀致性的平台,所以EVM⼜叫world wide compute,EVM的寻址空间是⾮常⼤的,是256位的,像如之前讲的uint和signed int就是256位的,普通计算机是64位的。
汽油费
⽐特币和以太坊两种区块链模型的设计理念是有很⼤差别的,⽐特币的设计理念是简单,脚本语⾔的功能很有限,不⽀持循环。⽽以太坊是要提供⼀个图灵完备的编程模型。很多功能在⽐特币系统上实现不了或者⽐较困难,在以太坊中实现起来却是⾮常容易。当然这样也会带来⼀些问题,⽐如说出现死循环怎么办,当⼀个全节点收到⼀个对智能合约的调⽤,怎么知道这个调⽤执⾏起来会不会导致死循环,有什么解法吗?
没有,这是⼀个 halting problem(停机问题)。
停机问题是不可解的,需要注意⼀下这个问题不是NPC的(Non-deterministic Polynomial的问题,即多
项式复杂程度的⾮确定性问题),NPC的问题是可解的,只不过没有多项式时间的解法,很多NPC问题有很多⾃然的指数时间的解法,⽐如哈密尔顿回路问题,把所有可能性枚举⼀遍,n个顶点的排列是n!,把每个组合检查⼀下是不是构成⼀个合法的回路,就知道它有没有哈密尔顿回路,哈密尔顿回路是可解的,只不过解的复杂度是指数级的。停机问题已经从理论上证明不存在这样的算法能够对任意给定的输⼊程序判断出这个程序是否会停机,这是不可解的。
以太坊中如何解决的呢?
把这个问题推给发起交易的账户,以太坊引⼊了汽油费机制,你发起⼀个对智能合约的调⽤需要⽀付相应的汽油费。
不同指令消耗的汽油费不同,简单指令例如加减法消耗的汽油费⽐较少,复杂的指令消耗的⽐较多。
⽐如说取哈希,这个运算虽然⼀条指令就可以完成,但是汽油费就⽐较贵。除了计算量之外,需要存储状态的指令消耗的汽油费也是⽐较⼤的。相⽐之下,如果只是为了读取公共数据,那些指令是可以免费的。
交易的数据结构:
AccountNonce是交易的序号,⽤于防⽌前⾯说到的replay attack(重放攻击),price是单位汽油的价格,Gaslimit是这个交易愿意⽀付的最⼤汽油量,相乘之后就是这个交易可能消耗的最⼤汽油费。
recipient是收款⼈的地址,amount的转账⾦额,可以看到交易中汽油费跟转账⾦额是分开的。
payload就是之前说的data域,⽤于存放调⽤的是合约中哪⼀个函数以及函数的参数取值是什么。
当⼀个全节点收到⼀个对智能合约的调⽤的时候,先按照这个调⽤给出的gas limit算出可能花掉的最⼤汽油费,然后⼀次性把汽油费从发起调⽤的账户中扣掉,然后再根据实际执⾏情况算出实际花了多少汽油费,汽油费不够会引起回滚。
错误处理
以太坊中的交易执⾏起来具有原⼦性,⼀个交易要么全部执⾏要么完全不执⾏,不会只执⾏⼀部分。这个交易既包含普通的转账交易也包含对智能合约的调⽤,所以如果在执⾏智能合约过程中出现任何错误,会导致整个交易的执⾏回滚,退回到开始执⾏之前的状态,就好像这个交易完全没有执⾏过。
错误处理⼀
之前所说的汽油费,如果这个交易执⾏完之后没有达到当初的gaslimit,那么多余的汽油费会被退回到这个账户⾥;相反的,如果执⾏到⼀半,gaslimit⽤完了,合约的执⾏要退回到开始执⾏之前的状态,⽽且这个时候已经消耗的汽油费是不退的。为什么这么设计呢?防⽌⼀些恶意的节点发动denial service attack,发动⼀个计算量很⼤的合约然后不停地调⽤这个合约,每次调⽤的时候给的汽油费都不够,反正最后汽油费都会退回来,对恶意节点来说没什么损失,但是对矿⼯来说⽩⽩浪费了很多资源。
上⾯提到处理交易矿⼯会浪费很多资源,是因为每个矿⼯接受到交易后放到交易池中,验证交易和其他的条件验证,然后将交易组装成区块,对块进⾏⼯作量证明。
⼀个区块上的所有交易是由出块成功的那个矿⼯处理,然后这个矿⼯会得到这个区块的奖励和这个快包含的所有交易的⼿续费。
错误处理⼆
assert语句和require语句,这⾥两个语句都是⽤来判断判断某种条件,如果条件不满⾜的话就会导致抛出异常。
assert语句⼀般来说是⽤来判断某种内部条件,和c语⾔中的类似;
reuire语句判断某种外部条件,⽐如说判断函数的输⼊是否符合要求,下图所给的例⼦是bid函数⾥,判
断当前时间now是否⼩于等于拍卖结束时间,如果符合条件,继续执⾏,不符合,即拍卖时间已经结束了,这个时候就会抛出异常。
错误处理三
revert语句⽆条件抛出异常,如果执⾏到revert语句,那么他⾃动的就会导致回滚,早期版本⽤的是throw语句,新版本的solidity建议改为revert语句。
⼀些语⾔像java⽤户可以⾃⼰定义出现错误怎么办,但是solidity没有try-catch结构。
嵌套调⽤
Q1: 前⾯说智能合约调⽤出现错误会导致回滚,那么如果是嵌套调⽤,⼀个智能合约调⽤另外⼀个智能合约,被调⽤的智能合约出现错误是不是会导致发起调⽤的智能合约也跟着⼀起回滚呢?叫做连锁式回滚。
不⼀定,这个取决于调⽤智能合约的⽅式,如果这个智能合约是直接调⽤的,那么它会触发连锁式的回滚,整个交易都会回滚。如果是⽤call()这种⽅式调⽤,他就不会引起连锁式回滚,只会使当前的调⽤失败,返回⼀个False的返回值。
Q2:有些情况下,从表⾯上看,你并没有调⽤任何函数,⽐如说单纯的账户转账,但是如果这是以个合约账户的话,转账的本⾝就有可能触发对函数的调⽤,为什么呢?
因为有fallback函数,这就是⼀种嵌套调⽤,⼀个合约往另外⼀个合约⾥转账,就可能调⽤这个合约⾥的fallback函数。
(如果是⽤call()这种⽅式给合约转账,合约⾥没有fallback函数,也没有说明调⽤哪个函数,call本⾝就会返回false,但是不会引起连锁式回滚。)
数据结构
blockheader
GasUsed是这个区块⾥,所有交易所消耗的汽油费加在⼀起
GasLimit是这个区块⾥所有交易能够消耗汽油的上限,这⾥和每个交易的gaslimit(⾃⼰设定的)是不⼀样的。
解释:⽐特币中规定每个区块不能超过1M,是写在协议⾥不能更改的,⽐特币的交易是⽐较简单的,基本上可以⽤交易的字节数来衡量出这个交易消耗的资源有多少,但是以太坊这么规定是不⾏的,因为智能合约的逻辑很复杂,有的交易从字节上看可能很⼩,但是它消耗的资源很⼤,⽐如它可能调⽤别的合约之类的,所以怎么办呢?要根据交易的具体操作来收费,这就是汽油费设置的gaslimit。
以太坊的上限GasLimit,和⽐特币不太⼀样,每个矿⼯在发布区块的时候可以对这个GasLimit进⾏微调,它可以在上⼀个区块的GasLimit上调或者下调1/1024,这种机制实际求出的系统GasLimit是所有矿⼯认为⽐较合理的GasLimit的⼀个平均值。
Q1: 假设某个全节点要打包⼀些交易到⼀个区块⾥⾯,这些交易⾥有⼀些是对智能合约的调⽤,那么这个全节点是应该先把智能合约都执⾏完之后再去挖矿呢?还是先挖矿获得记账权再去执⾏智能合约?
(?全节点和矿⼯⼀样吗?)(矿⼯是使⽤全部区块链数据)
(发布⼀个交易后每个节点都需要执⾏,否则状态就不同步更新了,发布智能合约后要扣汽油费,只是在全节点本地扣⼀下⽽已)
答:先执⾏智能合约再挖矿,以太坊挖矿需要尝试不同的nonce值,到⼀个符合要求的,计算哈希的时候要⽤到blockheader的内容,包含三棵树的根哈希值,只有执⾏完区块中的所有交易包括智能合约交
易,这样才能更新这三棵树,知道三个根哈希值,blockheader的内容才能确定,然后才能尝试各个nonce挖矿。
Q2:全节点在收到⼀个对智能合约的调⽤的时候,要⼀次性先把这个调⽤可能花掉的最⼤汽油费从发起这个调⽤的账户扣掉,这个具体是怎么操作的?
三棵树,状态树,交易树和收据树都是全节点在本地维护的数据结构,状态树记录了每个账户的状态,包括账户余额,汽油费是全节点收到调⽤的时候从本地维护的数据结构⾥把他账户的余额减掉就⾏了,只有区块发布之后本地修改才会变成外部可见的,才会变成区块链的共识。
Q3:矿⼯在挖矿执⾏智能合约的过程中消耗了很多本地资源,但是并没有获得记账权,没有出块奖励,也不会得到汽油费奖励,怎么办?
答:没有办法,以太坊中就是没有补偿,还需要把别⼈发布的区块⾥的交易在本地执⾏⼀遍,以太坊规定要验证发布区块的正确性,每个全节点要独⽴验证,把别⼈发布的交易区块在本地执⾏⼀遍,更新三棵树的内容算出根哈希值,再和发布的新区块的根哈希值⽐较是否⼀致。这种机制下挖矿慢的矿⼯就特别吃亏,汽油费的设置本来是对矿⼯执⾏智能合约所消耗的资源的⼀种补偿,但是这种补偿只有挖到矿的矿⼯才能得到,其他矿⼯得不到。
Q4:如果不验证会造成什么影响?如何改进?
答:会直接威胁到区块链的安全,区块链的安全保证是来⾃所有全节点独⽴验证发布的区块的合法性,这样少数有恶意的节点才没有办法篡改这些内容,如果⼀些矿⼯想不通,不给钱就不验证了,这样就会危及到区块链的安全。
这样是不可⾏的,因为如果跳过验证步骤,以后就没法挖矿,因为验证的时候需要把区块的交易都执⾏⼀遍,更新本地的三棵树,获取最新的根哈希值,如果不验证的话,本地三棵树的内容没有办法更新,以后就没办法发布新的区块了。
因为发布的区块没有三棵树的内容,只是块头⾥有个根哈希值,是看不到树的具体内容的,也就没办法更新本地账户,所以没有办法不验证的。
在矿池⾥,矿⼯本⾝就不验证了,有⼀个全节点pool manager负责统⼀验证,矿⼯相信全节点验证的正确性,全节点分配给矿⼯看到的是puzzle的内容,puzzle是全节点跟着区块链跟新得来的。
Q5:发布到区块链上的交易是不是都是成功执⾏的?如果智能合约在执⾏中出现错误,要不要也发布在区块链上?
执⾏发⽣错误的交易也要发布到区块链上,否则没有办法扣掉汽油费。
Q6:怎么知道这个交易是执⾏成功了呢?
三棵树⾥⾯,每个交易执⾏完之后形成⼀个收据,下图是收据的内容,其中status域会告诉你这个交易的执⾏情况。
Receipt数据结构
status域会告诉你这个交易的执⾏情况,成功或者失败。
Q7:智能合约⽀不⽀持多线程?多核并⾏处理。
Solidity不⽀持多线程,没有多线程的语句。
以太坊是⼀个交易驱动的状态机,这个状态机必须是完全确定性的,即给定⼀个智能合约,⾯对同⼀种输⼊,产⽣的输出或者是转移到下⼀个地⽅的状态必须是完全确定的。因为所有全节点都得执⾏同⼀组操作,到达同⼀个状态,要验证。如果状态不确定的话那三棵树的根哈希值对不上,所以必须完全确定才⾏。
多线程的问题在于,多个核对内存访问顺序不同,执⾏结果有可能是不确定的,所以solidity是不⽀持多线程的。除了多线程,其他所有可能造成结果不确定的操作也都不⽀持,⽐如产⽣随机数。所以以太坊
中没有办法真正产⽣随机数,只能产⽣伪随机数,否则的话⼜会出现前⾯的问题,每个全节点执⾏完⼀遍得到的结果都不⼀样。
智能合约可以获得的信息
可获得的区块信息
智能合约的执⾏必须是确定性的,这也就导致了智能合约不能像通⽤的编程语⾔那样通过系统调⽤得到⼀些system call的⼀些环境信息,因为每个全节点的执⾏环境 不是完全⼀样的,所以他只有通过⼀些固定的变量的值能够得到⼀些状态信息,这个表格就是智能合约能够得到的区块链的⼀些信息。
可获得的调⽤信息
⽐如说外部账户A,调⽤合约B,合约中有⼀个函数f1,f1⼜调⽤另外⼀个合约C,⾥⾯有⼀个函数f2,那么对这个f2函数,msg.sender是B合约,tx.origin是账户A。
msg.gas是当前这个调⽤还剩下多少汽油费,这个决定了我还能做哪些操作。包括你要想再调⽤别的合约,前提是还有⾜够的汽油费剩下来
msg.data就是数据域,⾥⾯写了调⽤的函数和这个函数的参数取值
soliditymsg.signature是msg.data的前四个字节,就是函数标识符,调⽤的是哪个函数。
now和timestamp是⼀个意思,智能合约⾥没有办法获得很精确的时间,只能获得跟这个当前区块的⼀些信息时间。
地址类型
第⼀个是成员变量:就是成员账户的余额balance, uint256是成员变量的类型,不是函数调⽤(参数),单位是⽐较⼩的。addr.balance()这个地址上账户的余额。
剩下的都是成员函数,成员函数的语义和直观理解不太相同,ansfer(12345)不是addr这个地址向外转出多少钱,⽽是当前这个合约C向这个地址转⼊多少钱。
addr.call是指当前这个合约发起⼀个调⽤,调⽤的是addr这个合约。
拍卖的例⼦
简单拍卖
拍卖规则:拍卖结束之前每个⼈都可以去出价去竞拍,竞拍的时候为了保证诚信,需要把竞拍的价格相应的以太币发过去,⽐如出⼀百个以太币,你⽤bid函数竞拍的时候,要把100个以太币发送到智能合约,并锁在⾥⾯直到拍卖结束,不允许中途退出,可以加价,拍卖结束之后,highestBidder(最⾼出价者)的出价的钱数会给受益⼈beneficiary,受益⼈应该把拍卖物也给最⾼出价⼈。其他没有竞拍成功的⼈可以把钱再取出来。竞拍可以多次出价,补差价发到智能合约⾥就可以,出价有效的话必须保证加价之后的出价⾼于之前的最⾼出价,否则就是⽆效(⾮法)的。
constructor构造函数在合约创建的时候会记录受益⼈是谁,结束时间是什么时候。
下⾯两个是拍卖⽤的两个函数:
左边是竞拍bid函数,竞拍的时候发起⼀个交易调⽤拍卖合约中的bid函数,bid虽然没有参数,但是在msg.value发起这个调⽤的时候转账转过去的以太币数⽬,就是出的竞拍价格。
⾸先查⼀下上⼀次的出价加上当前调⽤所发过去的以太币⼤于最⾼出价,如果是以前没有出价过,第⼀部分就是0。
bids是个哈希表,solidity⾥的特点是如果要查询的键值不存在,则返回默认值为0。所以如果是以前没有出价过,第⼀部分就是0
第⼀次拍卖的时候把拍卖者的信息放在bidder数组⾥,因为solidity不⽀持遍历,要遍历哈希表必须保存⼀下包含哪些元素,然后记录⼀下新的最⾼出价⼈是谁,写⼀些⽇志之类的。
右边是拍卖结束后的合约。
先查询⼀下拍卖是否结束,如果已经结束还参与拍卖则抛出异常。
第⼆⾏判断这个函数是不是被调⽤过,如果调⽤过就不⽤再调⼀遍了。
第三⾏把最⾼出价⼈的钱转给受益⼈,对于拍卖没有成功的⼈,最后循环把⾦额退回给bidder。
最后标注⼀下这个函数已经执⾏完了,写⼀个log。
如果要搞⼀个竞拍要写⼀个solidity程序,然后发布⼀个交易,把这个合约放到⽹上,别⼈怎么知道这个合约是需要⾃⼰线下宣传的,区块链是不管的,然后别⼈知道合约地址后进⾏竞拍,都是在区块链上通过转账交易执⾏的
智能合约的代码是储存在data域⾥⾯的,矿⼯把智能合约发布到区块链上之后返回给你⼀个合约的地址,然后这个合约就在区块链上了,所有⼈都可以调⽤。
任何⼈出价竞拍调⽤Bid函数的操作都需要矿⼯发布在区块链上。
这⾥有个问题就是AuctionEnded函数必须有⼈调⽤才会执⾏,执⾏之后才会结束,solidity语⾔没有办法把他设置成为拍卖结束之后⾃动执⾏end。
⿊客使拍卖失败
⿊客可以利⽤fallback实现整个拍卖的失败。
假设有⼀个⼈通过上图左边的合约账户参与竞拍,会有什么结果?
这个合约只有⼀个函数,hack_bid,参数是拍卖合约的地址,把它转成拍卖合约的实例,然后调⽤拍卖合约的bid函数,把钱发送过去。这是⼀个合约账户,合约账户不能⾃⼰发起交易,得有⼀个⿊客从他⾃⼰的外部账户发起⼀个交易,调⽤这个合约的hack_bid函数,这个函数再调⽤拍卖合约的bid函数,把他⾃⼰收到的转过来的钱,⿊客外部转过来的钱,再转给拍卖合约中的bid函数就参与拍卖了。
参与拍卖没有问题,但是退款会有问题,转给这个合约账户的钱会有什么情况?
⿊客外部账户对于拍卖合约是不可见的,拍卖合约能看到的只是⿊客合约,这⾥的退款转账函数没有调⽤任何函数,当⼀个合约账户收到转账没有调⽤任何账户的时候,应该调⽤fallback函数,但是这个函数没有定义fallback函数,会调⽤失败并抛出异常,transfer函数会引起连锁式回滚,导致转账操作失败收不到钱。
转账的过程是全节点执⾏到beneficiarytransfer的时候把相应账户的余额进⾏了调整,所有的solidity语句即智能合约执⾏过程中的任何语句对状态的修改该的都是本地的状态和本地的数据结构。所以这个循环当中不论是排在⿊客合约顺序前⾯还是后⾯都是在改本地数据结构,只不过排在后⾯的bidder根本没有
机会来得及执⾏,然后整个都回滚了,就好像这个智能合约从来没有执⾏,所以所有⼈都收不到钱。出现这种情况怎么办?
没有办法,code is law,智能合约的规则是由代码逻辑决定的,代码⼀旦发布到区块链上就改不了了,这样的好处是没有⼈能够篡改规则,坏处是出现漏洞也⽆法修改。智能合约如果设计的不好的话有可能把以太币永久的锁起来谁也取不了。有点像irrevocable trust不可撤销的信托。
能不能给智能合约留个后门给开发者⽤来修复bug?
构造函数加⼀个域owner,记录⼀下owner是谁,然后对这个owner的地址允许他做⼀些类似系统管理员的操作,⽐如可以任意转账。出现Bug之后超级管理员就可以把锁定的钱转出来。这样做的前提是所有⼈应该信任这个⼈,否则他有可能携款逃⾛。那有什么其他改进⽅法吗?
改进版本
把前⾯的auctionend函数拆成两个函数,左边是withdraw右边是beneficiary。
withdraw是说不⽤循环了,每个竞拍失败的⼈⾃⼰调⽤withdraw函数把钱取出来。判断这个⼈是不是最⾼出价者,是的话不能退钱。判断账户余额是不是正的,amount就是账户余额,if 把账户余额转给msg.sender,就发起调⽤的⼈,然后把账户余额清0,免得下次再取钱。
pay2Beneficiary把最⾼出价给受益⼈。
改进版本的问题——重⼊攻击
右边是⿊客合约,hack_bid就和前⾯的合约hack_bid是⼀样的,通过调⽤拍卖合约的bid函数参与竞拍,hack_withdraw就在拍卖结束的时候调⽤withdraw函数,把钱取回来。问题在于右边最后⼀个函数fallback函数⼜把钱取了⼀遍。
hack_withdraw调⽤拍卖合约的withdraw函数的时候,左边执⾏到if(msg.sender)会向⿊客合约转账,并调⽤了msg.sender地址的fallback函数,msg.sender就是⿊客的合约,把他当初出价的⾦额转给他;
在运⾏⿊客合约的fallback函数时,⼜调⽤了拍卖函数的withdraw函数去取钱,这⾥的msg.sender是拍卖合约,因为是拍卖合约把钱转给这个合约的,左边的拍卖合约⼜开始执⾏到
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论