当ether被送到合约后,要么执行fallback函数或者执行合约里指定的其余函数。可是在智能合约中有2种异常状况。在这两种异常状况下,不用执行合约里的任何代码就能够操做合约里的ether。智能合约编程时,若是认为只有执行合约代码才能操做ether的话,就会致使合约收到攻击:ether能够被强制性的送到一个指定的合约。html
攻击原理程序员
在程序设计中,有一种通用的技术,即预先设置几个不变的状态,在相应的程序执行完后,再来确认状态没有发生变化。一个普通的例子就是ERC20 token标准里的totalSupply变量, 由于没有函数会去修改totalSupply,开发程序员能够在transfer()里加上一个检查:在确信totalSupply不变的前提下,保证程序的正常执行。编程
有一个不变的状态,特别容易被开发程序员用到,可是也特别容易被外部用户操纵。这个状态就是当前合约里的ether数量。一般,做为刚刚接触Solidity的程序员,每每会有一个迷思:只有合约里payable的函数才能发送和接受ether。这个迷思致使一个虚假,其实是不正确的关于合约中ether数量的想定。最明显的带有漏洞的用法就是this.balance。像下面所显示的,对this.balance不当使用会致使很严重的漏洞。并发
在两种状况下,ether会被强制性的送给一个合约,既不会用到payable 修饰符也不会执行任何合约代码。下面咱们具体讨论。app
异常状况1:自毁/自杀(Self Destruct / Suicide)ide
智能合约能够实现selfdestruct(address)函数,这个函数会删除合约地址里的全部二进制代码而且把合约里全部的ether送到一个能够指定参数的地址。若是指定的地址也是一个智能合约的话,就不会调用合约里任何的函数(包含fallback) 于是, selfdestruct()函数能被用来强制性的发送ether到任何合约,而不会执行合约里的任何代码:合约里的任何payable 函数都不会执行。这意味着:攻击者能够建立一个带有selfdestruct()的合约,同时发送ether给这个合约,调用selfdestruct(target),而后在强制性的发送ether给一个指定的合约。这篇文章有很详细的描述:http://swende.se/blog/Ethereum_quirks_and_vulns.html 函数
异常状况2:Pre-sent Etherui
第二种方法是预导入(pre-load)带有ether的合约。合约地址是肯定的:合约地址是基于建立合约地址的哈希以及建立合约的交易的nonce两个信息计算而来。this
address = sha3(rlp.encode([account_address,transaction_nonce])) spa
这意味着,任何人均可以在合约被建立前计算合约的地址,并发送ether给那个地址。而后当合约真正被建立是,合约就会有一个非零的ether余额
咱们能够经过下面一个简单的合约代码来看看如何利用上面的知识来找到合约里的漏洞。
EtherGame.sol
contract EtherGame { uint public payoutMileStone1 = 3 ether; uint public mileStone1Reward = 2 ether; uint public payoutMileStone2 = 5 ether; uint public mileStone2Reward = 3 ether; uint public finalMileStone = 10 ether; uint public finalReward = 5 ether; mapping(address => uint) redeemableEther; // users pay 0.5 ether. At specific milestones, credit their accounts function play() public payable { require(msg.value == 0.5 ether); // each play is 0.5 ether uint currentBalance = this.balance + msg.value; // ensure no players after the game as finished require(currentBalance <= finalMileStone); // if at a milestone credit the players account if (currentBalance == payoutMileStone1) { redeemableEther[msg.sender] += mileStone1Reward; } else if (currentBalance == payoutMileStone2) { redeemableEther[msg.sender] += mileStone2Reward; } else if (currentBalance == finalMileStone ) { redeemableEther[msg.sender] += finalReward; } return; } function claimReward() public { // ensure the game is complete require(this.balance == finalMileStone); // ensure there is a reward to give require(redeemableEther[msg.sender] > 0); redeemableEther[msg.sender] = 0; msg.sender.transfer(redeemableEther[msg.sender]); } }
这个智能合约是一个简单的游戏:玩家发送0.5给合约,并争取成为第一个完成3个里程碑任务的人。里程碑任务是以ether标价的:完成第一个里程碑任务的第一我的(好比合约总额达到5 ether时的第一人),就会在游戏结束后拿回一部分的ether。当最终的里程碑任务(好比合约总额达到10ether时)达成时,游戏结束,相应的玩家拿到奖赏。
问题在于合约里这几行代码:
… uint currentBalance = this.balance + msg.value; // ensure no players after the game as finished require(currentBalance <= finalMileStone); … require(this.balance == finalMileStone); …
攻击者能够经过selfdestruct()来强制性的送小额的ether(好比0.1个ether)给合约,这样的话,全部未来的玩家都将不能达成里程碑任务。由于全部正规的玩家都只会发送0.5或者0.5倍数的ether。一旦合约接受了上面的0.1ether,this。balance将永远不会为0.5的倍数。因此下列的if条件永远都不会是true
… // if at a milestone credit the players account if (currentBalance == payoutMileStone1) { redeemableEther[msg.sender] += mileStone1Reward; } else if (currentBalance == payoutMileStone2) { redeemableEther[msg.sender] += mileStone2Reward; } else if (currentBalance == finalMileStone ) { redeemableEther[msg.sender] += finalReward; } …
更要命的是,若是一个报复心很强的攻击者,若是错过了一个里程碑任务的话,他能够强制性的送10 ether (或者让合约余额超过finalMileStone数目的ether),这就将永远锁住合约里的全部奖励。这是由于claimReward()函数会由于下面的require语句老是回退(revert) (this.balance 老是大于 finalMileStone).
// ensure the game is complete require(this.balance == finalMileStone);