the security of smart contract- 1
https://blog.zeppelin.solutions/the-hitchhikers-guide-to-smart-contracts-in-ethereum-848f08001f05
这个是简单的介绍
Hitchhiker’s Guide to Smart Contracts in Ethereum
Smart Contract security in Ethereum is hard
“Smart contracts are pretty difficult to get right.” Emin Gün Sirer
Given that by nature, smart contracts are computer code that define how money moves, I couldn’t end this guide without even a small note on security. I’ll be talking about smart contract security in much more depth at future posts (edit: like this one), but here are some quick notes to get you started.
Some problems you should be aware of (and avoid):
- Reentrancy: Do not perform external calls in contracts. If you do, ensure that they are the very last thing you do.
- Send can fail: When sending money, your code should always be prepared for the send function to fail.
- Loops can trigger gas limit: Be careful when looping over state variables, which can grow in size and make gas consumption hit the limits.
- Call stack depth limit: Don’t use recursion, and be aware that any call can fail if stack depth limit is reached. EDIT: this is no longer a problem.
- Timestamp dependency: Do not use timestamps in critical parts of the code, because miners can manipulate them.
These are provided just as examples of unexpected behaviors that can lead for theft or destruction of funds in your smart contract. The moral is: if you’re writing smart contracts, you’re writing code that handles real money. You should be very careful! Write tests, do code reviews, and audit your code.
The best way to avoid obvious security problems is to have a solid understanding of the language. I recommend you read the Solidity documentation if you have the time. We still need better tools for acceptable smart contract security. (Edit: close to the original publishing of this post, we launched the OpenZeppelin library, and we’ve recently announced zeppelinOS.
https://blog.zeppelin.solutions/onward-with-ethereum-smart-contract-security-97a827e47702
这个是详细教你怎么做
https://github.com/ConsenSys/smart-contract-best-practices/blob/master/README-zh.md
以太坊智能合约 —— 最佳安全开发指南
现在Live Libs 和Zeppelin Solidity 正寻求提供安全的智能合约组件使其能够被重用而不需要每次都重新编写。任何合约安全性分析都必须标明重用代码,特别是以前没有建立与目标智能合同系统中处于风险中的资金相称的信任级别的代码。
1)
外部调用
尽量避免外部调用
调用不受信任的外部合约可能会引发一系列意外的风险和错误。外部调用可能在其合约和它所依赖的其他合约内执行恶意代码。因此,每一个外部调用都会有潜在的安全威胁,尽可能的从你的智能合约内移除外部调用。当无法完全去除外部调用时,可以使用这一章节其他部分提供的建议来尽量减少风险。
仔细权衡“send()”、“transfer()”、以及“call.value()”
当转账Ether时,需要仔细权衡“someAddress.send()”、“someAddress.transfer()”、和“someAddress.call.value()()”之间的差别。
x.transfer(y)
和if (!x.send(y)) throw;
是等价的。send是transfer的底层实现,建议尽可能直接使用transfer。someAddress.send()
和someAddress.transfer()
能保证可重入 安全 。 尽管这些外部智能合约的函数可以被触发执行,但补贴给外部智能合约的2,300 gas,意味着仅仅只够记录一个event到日志中。someAddress.call.value()()
将会发送指定数量的Ether并且触发对应代码的执行。被调用的外部智能合约代码将享有所有剩余的gas,通过这种方式转账是很容易有可重入漏洞的,非常 不安全。
使用send()
或transfer()
可以通过制定gas值来预防可重入, 但是这样做可能会导致在和合约调用fallback函数时出现问题,由于gas可能不足,而合约的fallback函数执行至少需要2,300 gas消耗。
一种被称为push 和pull的 机制试图来平衡两者, 在 push 部分使用send()
或transfer()
,在pull 部分使用call.value()()
。(*译者注:在需要对外未知地址转账Ether时使用send()
或transfer()
,已知明确内部无恶意代码的地址转账Ether使用call.value()()
)
需要注意的是使用send()
或transfer()
进行转账并不能保证该智能合约本身重入安全,它仅仅只保证了这次转账操作时重入安全的。
介绍send,transfer,call.value:
- balance 和 transfer
可以通过地址的balance属性来查看一个地址的余额,发送以太币(单位为:wei)到一个地址可以使用 transfer方法
address x = 0x123;
address myAddress = this;
if (x.balance < 10 && myAddress.balance >= 10) x.transfer(10);//将合约this中的10wei转到账户地址x
注意:如果x是一个合约地址,它的代码(如果存在的话,更明确是为 fallback 函数)将会和 transfer 调用一起被执行,send也一样(这是EVM的限制,是不可阻止的)。如果执行过程中gas不够或是失败,当前合约会终止抛出异常
-
send
send方法和transfer很相似,但是比transfer更低级。如果send失败,当前的合约不会中断,也不会抛出异常,会返回一个false,所以需要使用if进行判断:
if (!msg.sender.send(refund)) { refunds[msg.sender] = refund; // reverting state because send failed }
注意:使用send有一些风险:如果调用栈深度超过1024或是gas不够,所有的转让操作都会失败,为了更安全的以太币转移,如果用send就必须每次都要检查返回值,所以使用transfer方法会更好;
- call, callcode, delegatecall
而且,call方法可以和没有依附于ABI上的合约进行交互(就是要传递value和gas时),它提供了任意多个任意类型的参数。这些参数填充成32字节,并连接起来。有情况如果第一个参数被加密成4个字节,就会抛出异常,这种情况下,是不允许使用这个方法。
call 返回一个boolean值,被调用函数终止返回true或是有EVM异常就返回false。不会返回访问的具体数据(对此,我们需要预先知道编码方式和大小)。
同样的方式,delegatecall也可以使用,和call的唯一区别是 delegatecall会使用给定地址的代码。所有其他方面(存储,余额等)都是从当前合约中获取。delegatecall的作用是使用其他合约里的library代码。用户需要确保两个合约的存储布局适用于 deletegatecall的使用。
call, delegatecall和call都是很低层次的方法,只有在实在没办法的时候才使用,这三个方法会破坏Solidity里的类型安全。
三个方法都可以使用 .gas(),而 .value() 不适合deletegatecall.
注意:所有的合约都继承了地址相关成员方法。所以可以使用 this.balance 来查询当前合约的余额
⚠️ 这些方法都是低层数的方法,使用的时候一定要小心。如果使用不当,任何未知的合约都可能别破坏。 你应该移交控制到可以通过回调到你自己合约的那个合约里,这样通过返回值就可以更新你自己的state变量
⚠️综上所述,如果需要交互(即使用 .gas()和 .value()),使用call(),要有if进行判断;否则使用transfer()
处理外部调用错误
Solidity提供了一系列在raw address上执行操作的底层方法,比如: address.call()
,address.callcode()
, address.delegatecall()
和address.send
。这些底层方法不会抛出异常(throw),只是会在遇到错误时返回false。(所以需要if判断)
另一方面, contract calls (比如,ExternalContract.doSomething()
))会自动传递异常,(比如,doSomething()
抛出异常,那么ExternalContract.doSomething()
同样会进行throw
) )。
如果你选择使用底层方法,一定要检查返回值来对可能的错误进行处理。
// bad someAddress.send(55); someAddress.call.value(55)(); // this is doubly dangerous, as it will forward all remaining gas and doesn't check for result someAddress.call.value(100)(bytes4(sha3("deposit()"))); // if deposit throws an exception, the raw call() will only return false and transaction will NOT be reverted // good if(!someAddress.send(55)) { // Some failure code } ExternalContract(someAddress).deposit.value(100);//当一个函数声明为payable,传递value的方法;
//若要设置gas,则写为
ExternalContract(someAddress).deposit.gas(30000);
不要假设你知道外部调用的控制流程
无论是使用raw calls 或是contract calls,如果这个ExternalContract
是不受信任的都应该假设存在恶意代码。即使ExternalContract
不包含恶意代码,但它所调用的其他合约代码可能会包含恶意代码。一个具体的危险例子便是恶意代码可能会劫持控制流程导致竞态(race condition
对于外部合约优先使用pull 而不是push
外部调用可能会有意或无意的失败。为了最小化这些外部调用失败带来的损失,通常好的做法是将外部调用函数与其余代码隔离,最终是由收款发起方负责发起调用该函数。这种做法对付款操作尤为重要,比如让用户自己撤回资产而不是直接发送给他们。(译者注:事先设置需要付给某一方的资产的值,表明接收方可以从当前账户撤回资金的额度,然后由接收方调用当前合约提现函数完成转账)。(这种方法同时也避免了造成 gas limit相关问题。)
// bad contract auction { address highestBidder; uint highestBid; function bid() payable { if (msg.value < highestBid) throw; if (highestBidder != 0) { if (!highestBidder.send(highestBid)) { // if this call consistently fails, no one else can bid throw; } } highestBidder = msg.sender; highestBid = msg.value; } } // good contract auction { address highestBidder; uint highestBid; mapping(address => uint) refunds; function bid() payable external { if (msg.value < highestBid) throw; if (highestBidder != 0) { refunds[highestBidder] += highestBid; // record the refund that this user can claim } highestBidder = msg.sender; highestBid = msg.value; } function withdrawRefund() external { uint refund = refunds[msg.sender]; refunds[msg.sender] = 0;//先将临时账户清零,以免调用send时起触发fallback回退函数来递归调用withdrawRefund() if (!msg.sender.send(refund)) { refunds[msg.sender] = refund; // reverting state because send failed } } }
这上面是解释了为什么在合约中使用了临时账户的原因!!!!
临时账户:其实就是一个映射,在下面例子即deposits_,存储某address应该从合约中取出的钱数
下面这里的代码是openzepplin中写的有关实现临时账户的标准,值得学习,我们学习类似cryptopunks的代码的时候就发现他们是这么写的
https://github.com/OpenZeppelin/openzeppelin-solidity/tree/master/contracts/payment
Escrow.sol
pragma solidity ^0.4.23; import "../math/SafeMath.sol"; import "../ownership/Ownable.sol"; /** * @title Escrow * @dev Base escrow contract, holds funds destinated to a payee until they * withdraw them. The contract that uses the escrow as its payment method * should be its owner, and provide public methods redirecting to the escrow's * deposit and withdraw. */ contract Escrow is Ownable { using SafeMath for uint256; event Deposited(address indexed payee, uint256 weiAmount); event Withdrawn(address indexed payee, uint256 weiAmount); mapping(address => uint256) private deposits_; //得到临时账户中的余额 function depositsOf(address _payee) public view returns (uint256) { return deposits_[_payee]; } /** * @dev Stores the sent amount as credit to be withdrawn. * @param _payee The destination address of the funds. */
//往临时账户中存钱,因为声明为payable,所以调用该函数的address通过将msg.value数量的金额传给了合约地址,并同时将数额写到临时账户deposits_上 function deposit(address _payee) public onlyOwner payable { uint256 amount = msg.value;//为什么不直接用msg.value进行add运算,是这样更安全吗?????? deposits_[_payee] = deposits_[_payee].add(amount);//为了安全,不使用+来直接运算,而是使用SafeMath.sol中的函数 emit Deposited(_payee, amount); } /** * @dev Withdraw accumulated balance for a payee. * @param _payee The address whose funds will be withdrawn and transferred to. */
//从合约地址中取出临时账户上的所有钱,并将临时账户上的数额清零 function withdraw(address _payee) public onlyOwner { uint256 payment = deposits_[_payee]; assert(address(this).balance >= payment);//原本是this.balance来得到当前账户地址上的余额??????,address(this)就是是显示转换成合约地址为address类型 deposits_[_payee] = 0; _payee.transfer(payment); emit Withdrawn(_payee, payment); } }
疑惑address(this).balance的解决,(这里的this代表的是合约的地址):
pragma solidity ^0.4.24;
contract Get{
//查询当前的余额 function getBalance() public view returns(uint){ return this.balance; }
event SendEvent(address to, uint value, bool result);
//使用send()发送ether,观察会触发fallback函数
function sendEther() public{
bool result = this.send(1);
emit SendEvent(this, 1, result);
}
当想要使用this.balance这么写得到账户的余额时,remix编译中会报警告:
Warning:Using contract member "balance" inherited from the address type is deprecated.Convert the contract to "address" type to access the member,for example use "address(contract).balance" instead.
除此之外,使用this.send也是会报相似的警告:
Warning:Using contract member "send" inherited from the address type is deprecated.Convert the contract to "address" type to access the member,for example use "address(contract).send" instead.
这就是this.balance为什么改为address(this).balance的原因,在这里this.send也要改为address(this).send
PullPayment.sol
pragma solidity ^0.4.24; import "./Escrow.sol"; /** * @title PullPayment * @dev Base contract supporting async send for pull payments. Inherit from this * contract and use asyncTransfer instead of send or transfer. */ contract PullPayment { Escrow private escrow; constructor() public { escrow = new Escrow(); } /** * @dev Withdraw accumulated balance, called by payee. */
//取钱 function withdrawPayments() public { address payee = msg.sender; escrow.withdraw(payee); } /** * @dev Returns the credit owed to an address. * @param _dest The creditor's address. */
//查余额 function payments(address _dest) public view returns (uint256) { return escrow.depositsOf(_dest); } /** * @dev Called by the payer to store the sent amount as credit to be pulled. * @param _dest The destination address of the funds. * @param _amount The amount to transfer. */
//向临时账户中存钱 function asyncTransfer(address _dest, uint256 _amount) internal { escrow.deposit.value(_amount)(_dest);//形如someAddress.call.value()()
,因为deposit是payable的,value(_amount)相当于{value:_amount}
} }
其他的看openzeppelin-solidity/contracts的代码学习——payment
标记不受信任的合约
当你自己的函数调用外部合约时,你的变量、方法、合约接口命名应该表明和他们可能是不安全的。
// bad Bank.withdraw(100); // Unclear whether trusted or untrusted function makeWithdrawal(uint amount) { // Isn't clear that this function is potentially unsafe Bank.withdraw(amount); } // good UntrustedBank.withdraw(100); // untrusted external call TrustedBank.withdraw(100); // external but trusted bank contract maintained by XYZ Corp function makeUntrustedWithdrawal(uint amount) { UntrustedBank.withdraw(amount); }
使用assert()
强制不变性
当断言条件不满足时将触发断言保护 -- 比如不变的属性发生了变化。举个例子,代币在以太坊上的发行比例,在代币的发行合约里可以通过这种方式得到解决。断言保护经常需要和其他技术组合使用,比如当断言被触发时先挂起合约然后升级。(否则将一直触发断言,你将陷入僵局)
例如:
contract Token { mapping(address => uint) public balanceOf; uint public totalSupply; function deposit() public payable { balanceOf[msg.sender] += msg.value; totalSupply += msg.value; assert(this.balance >= totalSupply); } }
注意断言保护 不是 严格意义的余额检测, 因为智能合约可以不通过deposit()
函数被 强制发送Ether(下面有写)!
正确使用assert()
和require()
在Solidity 0.4.10 中assert()
和require()
被加入。require(condition)
被用来验证用户的输入,如果条件不满足便会抛出异常,应当使用它验证所有用户的输入。 assert(condition)
在条件不满足也会抛出异常,但是最好只用于固定变量:内部错误或你的智能合约陷入无效的状态。遵循这些范例,使用分析工具来验证永远不会执行这些无效操作码:意味着代码中不存在任何不变量,并且代码已经正式验证。
小心整数除法的四舍五入
所有整数除数都会四舍五入到最接近的整数。 如果您需要更高精度,请考虑使用乘数,或存储分子和分母。
(将来Solidity会有一个fixed-point类型来让这一切变得容易。)
// bad uint x = 5 / 2; // Result is 2, all integer divison rounds DOWN to the nearest integer // good uint multiplier = 10; uint x = (5 * multiplier) / 2; uint numerator = 5; uint denominator = 2;
记住Ether可以被强制发送到账户
谨慎编写用来检查账户余额的不变量。
攻击者可以强制发送wei到任何账户,而且这是不能被阻止的(即使让fallback函数throw
也不行)
攻击者可以仅仅使用1 wei来创建一个合约,然后调用selfdestruct(victimAddress)
。
在victimAddress
中没有代码被执行,所以这是不能被阻止的。
不要假设合约创建时余额为零
攻击者可以在合约创建之前向合约的地址发送wei。合约不能假设它的初始状态包含的余额为零。浏览issue 61 获取更多信息。
记住链上的数据是公开的
许多应用需要提交的数据是私有的,直到某个时间点才能工作。游戏(比如,链上游戏rock-paper-scissors(石头剪刀布))和拍卖机(比如,sealed-bid second-price auctions)是两个典型的例子。如果你的应用存在隐私保护问题,一定要避免过早发布用户信息。
例如:
- 在游戏石头剪刀布中,需要参与游戏的双方提交他们“行动计划”的hash值,然后需要双方随后提交他们的行动计划;如果双方的“行动计划”和先前提交的hash值对不上则抛出异常。
- 在拍卖中,要求玩家在初始阶段提交其所出价格的hash值(以及超过其出价的保证金),然后在第二阶段提交他们所出价格的资金。
- 当开发一个依赖随机数生成器的应用时,正确的顺序应当是(1)玩家提交行动计划,(2)生成随机数,(3)玩家支付。产生随机数是一个值得研究的领域;当前最优的解决方案包括比特币区块头(通过http://btcrelay.org验证),hash-commit-reveal方案(比如,一方产生number后,将其散列值提交作为对这个number的“提交”,然后在随后再暴露这个number本身)和 RANDAO。
- 如果你正在实现频繁的批量拍卖,那么hash-commit机制也是个不错的选择。
权衡Abstract合约和Interfaces
Interfaces和Abstract合约都是用来使智能合约能更好的被定制和重用。Interfaces是在Solidity 0.4.11中被引入的,和Abstract合约很像但是不能定义方法只能申明。Interfaces存在一些限制比如不能够访问storage或者从其他Interfaces那继承,通常这些使Abstract合约更实用。尽管如此,Interfaces在实现智能合约之前的设计智能合约阶段仍然有很大用处。另外,需要注意的是如果一个智能合约从另一个Abstract合约继承而来那么它必须实现所有Abstract合约内的申明并未实现的函数,否则它也会成为一个Abstract合约。
什么是Abstract合约:
合约中可以存在声明但未实现的函数(请注意,函数声明头被;
终止),见下面的例子:
pragma solidity ^0.4.0;
contract Feline {
function utterance() public returns (bytes32);
}
这样的合约不能被编译(即使它们同时包含具体函数和抽象函数),但它们可以用作父合约:
pragma solidity ^0.4.0;
contract Feline {//这就是abstract函数
function utterance() public returns (bytes32);
}
contract Cat is Feline {//继承它的合约必须要实现它未实现的函数
function utterance() public returns (bytes32) { return "miaow"; }
}
如果一个合约是从抽象合约中继承的,但没实现所有的函数,则它也是抽象合约。
在双方或多方参与的智能合约中,参与者可能会“脱机离线”后不再返回
不要让退款和索赔流程依赖于参与方执行的某个特定动作而没有其他途径来获取资金。比如,在石头剪刀布游戏中,一个常见的错误是在两个玩家提交他们的行动计划之前不要付钱。然而一个恶意玩家可以通过一直不提交它的行动计划来使对方蒙受损失 -- 事实上,如果玩家看到其他玩家泄露的行动计划然后决定他是否会损失(译者注:发现自己输了),那么他完全有理由不再提交他自己的行动计划。这些问题也同样会出现在通道结算。当这些情形出现导致问题后:
(1)提供一种规避非参与者和参与者的方式,可能通过设置时间限制
(2)考虑为参与者提供额外的经济激励,以便在他们应该这样做的所有情况下仍然提交信息。
使Fallback函数尽量简单
Fallback函数在合约执行消息发送没有携带参数(或当没有匹配的函数可供调用)时将会被调用,而且当调用 .send()
or .transfer()
时,只会有2,300 gas 用于失败后fallback函数的执行(译者注:合约收到Ether也会触发fallback函数执行)。如果你希望能够监听.send()
或.transfer()
接收到Ether,则可以在fallback函数中使用event(译者注:让客户端监听相应事件做相应处理)。谨慎编写fallback函数以免gas不够用。
// bad function() payable { balances[msg.sender] += msg.value; }//这样fallback函数将会消耗过多的gas,应该如下分开写来减小gas的使用 // good function deposit() payable external { balances[msg.sender] += msg.value; } function() payable { LogDepositReceived(msg.sender); }
明确标明函数和状态变量的可见性
明确标明函数和状态变量的可见性。函数可以声明为 external
,public
, internal
或 private
。 分清楚它们之间的差异, 例如external
可能已够用而不是使用 public
。对于状态变量,external
是不可能的。明确标注可见性将使得更容易避免关于谁可以调用该函数或访问变量的错误假设。
所以在remix编译时,如果省略了public就会产生警告,最好明确写出。
将程序锁定到特定的编译器版本
智能合约应该应该使用和它们测试时使用最多的编译器相同的版本来部署。锁定编译器版本有助于确保合约不会被用于最新的可能还有bug未被发现的编译器去部署。智能合约也可能会由他人部署,而pragma标明了合约作者希望使用哪个版本的编译器来部署合约。
// bad pragma solidity ^0.4.4; // good,这当然也会付出兼容性的代价 pragma solidity 0.4.4;
小心分母为零 (Solidity < 0.4)
早于0.4版本, 当一个数尝试除以零时,Solidity 返回zero 并没有 throw
一个异常。确保你使用的Solidity版本至少为 0.4。
区分函数和事件
为了防止函数和事件(Event)产生混淆,命名一个事件使用大写并加入前缀(我们建议Log)。对于函数, 始终以小写字母开头,构造函数除外。
// bad event Transfer() {} function transfer() {} // good event LogTransfer() {} function transfer() external {}
使用Solidity更新的构造器
更合适的构造器/别名,如selfdestruct
(旧版本为'suicide)和
keccak256(旧版本为
sha3)。 像
require(msg.sender.send(1 ether))``的模式也可以简化为使用transfer()
,如`msg.sender.transfer(1 ether)`。
已知的攻击
https://blog.csdn.net/blockchain_lemon/article/details/80102913
调用外部契约的主要危险之一是它们可以接管控制流,并对调用函数意料之外的数据进行更改。 这类bug有多种形式,导致DAO崩溃的两个主要错误——竞态和可重入都是这种错误。
我们会发现下面的这两类漏洞都发生在call.value()中,这是因为它传递给fallback函数可用的gas是当前剩余的所有gas,所以能够支持fallback函数进行递归调用,而send()和transfer()是对传递给fallback函数的gas进行过限制的,是2300gas,所以下面这两个问题的最好的解决方法就是使用transfer()或send()
漏洞一:函数可重入性
重入
这个版本的bug被注意到是其可以在第一次调用这个函数完成之前被多次重复调用。对这个函数不断的调用可能会造成极大的破坏。
// INSECURE mapping (address => uint) private userBalances; function withdrawBalance() public { uint amountToWithdraw = userBalances[msg.sender]; if (!(msg.sender.call.value(amountToWithdraw)())) { throw; } // At this point, the caller's code is executed, and can call withdrawBalance again userBalances[msg.sender] = 0; }
(译者注:使用msg.sender.call.value()())传递给fallback函数可用的气是当前剩余的所有气,在这里,假如从你账户执行提现操作的恶意合约的fallback函数内递归调用你的withdrawBalance()便可以从你的账户转走更多的币。)
可以看到当调msg.sender.call.value()()时,并没有将userBalances[msg.sender] 清零,于是在这之前可以成功递归调用很多次withdrawBalance()函数。 一个非常相像的bug便是出现在针对 DAO 的攻击。
意思就是说当在运行msg.sender.call.value()()时,此时如果多次递归地运行withdrawBalance()函数,那么就可以实现给地址msg.sender转多次钱,因为账户userBalances[msg.sender]一直没有清零
在给出来的例子中,最好的解决方法是 使用 send()
而不是call.value()()
。这将避免多余的代码被执行。
然而,如果你没法完全移除外部调用,另一个简单的方法来阻止这个攻击是确保你在完成你所有内部工作之前不要进行外部调用:
mapping (address => uint) private userBalances; function withdrawBalance() public { uint amountToWithdraw = userBalances[msg.sender]; userBalances[msg.sender] = 0; if (!(msg.sender.call.value(amountToWithdraw)())) { throw; } // The user's balance is already 0, so future invocations won't withdraw anything }
注意如果你有另一个函数也调用了 withdrawBalance()
, 那么这里潜在的存在上面的攻击,所以你必须认识到任何调用了不受信任的合约代码的合约也是不受信任的。继续浏览下面的相关潜在威胁解决办法的讨论。
如果想看更详细的例子,就接着看 the security of smart - 2
漏洞二:跨函数的竞态条件
攻击者也可以对共享相同状态的两个不同函数进行类似的攻击。
竞态*
竞态条件(race condition)就是指设备或系统出现不恰当的执行时序,而得到不正确的结果。
mapping (address => uint) private userBalances; function transfer(address to,uint amount) public { if(userBalance[msg.sender] >= amount){ userBalance[to] += amount; userBalance[msg.sender] -= amount; } } function withdrawBalance() public { uint amountToWithdraw = userBalances[msg.sender]; if (!(msg.sender.call.value(amountToWithdraw)())) { throw; } // At this point, the caller's code is executed, and can call withdrawBalance again userBalances[msg.sender] = 0; }
在这种情况下,攻击者可以在代码执行到调用withdrawBalance()的msg.sender.call.value(amountToWithdraw)()时调用transfer() 函数,由于他们的余额userBalances[msg.sender]在此时还未被置0,所以即使他们已经withdraw收到退款,但是他们也还能给别的账号转钱。
解决办法:
解决方案,这儿有两种解决方案,一是我们建议先完成所有的内部工作,然后再调用外部函数;二是使用互斥锁。
1.首先第一种解决方案,先完成所有的内部工作,然后再调用外部函数。如果你在编写智能合约时仔细地遵循这个规则,那么就可以避免出现竞态条件。但是,你不仅需要注意避免过早地调用外部函数,还要注意这个外部函数调用的外部函数,例如,下面的操作就是不安全的。
// INSECURE mapping (address => uint) private userBalances; mapping (address => bool) private claimedBonus; mapping (address => uint) private rewardsForA; function withdraw(address recipient) public { uint amountToWithdraw = userBalances[recipient]; rewardsForA[recipient] = 0; if (!(recipient.call.value(amountToWithdraw)())) { throw; } } function getFirstWithdrawalBonus(address recipient) public { if (claimedBonus[recipient]) { throw; } // Each recipient should only be able to claim the bonus once rewardsForA[recipient] += 100;
//因为当在调用recipient.call.value(amountToWithdraw)()时,可以递归多次调用getFirstWithdrawalBonus函数
//因为claimedBonus[recipient]一直没能设为true withdraw(recipient); // At this point, the caller will be able to execute getFirstWithdrawalBonus again. claimedBonus[recipient] = true; }
尽管函数getFirstWithdrawalBonus()不直接调用外部的合约,但在函数withdraw()中的调用足以使其进入竞态条件之中,因为它调用了recipient.call.value(amountToWithdraw)()。因此,你需要将函数withdraw()视为不可信函数。
mapping (address => uint) private userBalances; mapping (address => bool) private claimedBonus; mapping (address => uint) private rewardsForA; function untrustedWithdraw(address recipient) public { uint amountToWithdraw = userBalances[recipient]; rewardsForA[recipient] = 0; if (!(recipient.call.value(amountToWithdraw)())) { throw; } } function untrustedGetFirstWithdrawalBonus(address recipient) public { if (claimedBonus[recipient]) { throw; } // Each recipient should only be able to claim the bonus once claimedBonus[recipient] = true; rewardsForA[recipient] += 100; untrustedWithdraw(recipient); // claimedBonus has been set to true, so reentry is impossible }
除了修复漏洞使这种重入现象变得不可能外,还要标记出不可信的函数。这种标记要注意一次次的调用关系,因为函数untrustedGetFirstWithdrawalBonus()调用了不可信函数untrustedWithdraw(),这意味着调用了一个外部的合约,因此你必须将函数untrustedGetFirstWithdrawalBonus()也列为不可信函数。
2.第二中解决方案是使用互斥锁。即让你“锁定”某些状态,后期只能由锁的所有者对这些状态进行更改,如下所示,这是一个简单的例子:
// Note: This is a rudimentary example, and mutexes are particularly useful where there is substantial logic and/or shared state mapping (address => uint) private balances; bool private lockBalances; function deposit() payable public returns (bool) { if (!lockBalances) { lockBalances = true; balances[msg.sender] += msg.value; lockBalances = false; return true; } throw; } function withdraw(uint amount) payable public returns (bool) { if (!lockBalances && amount > 0 && balances[msg.sender] >= amount) { lockBalances = true; if (msg.sender.call(amount)()) { // Normally insecure, but the mutex saves it balances[msg.sender] -= amount; } lockBalances = false; return true; } throw; }
如果用户在第一次调用结束前尝试再次调用withdraw() 函数,那么这个锁定会阻止这个操作,从而使运行结果不受影响。这可能是一种有效的解决方案,但是当你要同时运行多个合约时,这种方案也会变得很棘手,以下是一个不安全的例子:
// INSECURE contract StateHolder { uint private n; address private lockHolder; function getLock() { if (lockHolder != 0) { throw; } lockHolder = msg.sender; } function releaseLock() { lockHolder = 0; } function set(uint newState) { if (msg.sender != lockHolder) { throw; } n = newState; } }
这种情况下攻击者可以调用函数getLock()锁定合约,然后不再调用函数releaseLock()解锁合约。如果他们这样做,那么合约将被永久锁定,并且永远不能做出进一步的更改。如果你使用互斥锁来防止竞态条件,你需要确保不会出现这种声明了锁定但永远没有解锁的情况。在编写智能合约时使用互斥锁还有很多其他的潜在风险,例如死锁或活锁。如果你决定采用这种方式,一定要大量阅读关于互斥锁的文献,避免“踩雷”。
有些人可能会反对使用竞态条件这个术语,因为以太坊并没有真正地实现并行性。然而,逻辑上不同的进程争夺资源的基本特征仍然存在,所以同样的漏洞和潜在的解决方案也同样适用。
交易顺序依赖(TOD) / 前面的先运行
以上是涉及攻击者在单个交易内执行恶意代码产生竞态的示例。
接下来演示在区块链本身运作原理导致的竞态:(同一个block内的)交易顺序很容易受到操纵。
由于交易在短暂的时间内会先存放到mempool中,所以在矿工将其打包进block之前,是可以知道会发生什么动作的。这对于一个去中心化的市场来说是麻烦的,因为可以查看到代币的交易信息,并且可以在它被打包进block之前改变交易顺序。避免这一点很困难,因为它归结为具体的合同本身。例如,在市场上,最好实施批量拍卖(这也可以防止高频交易问题)。 另一种使用预提交方案的方法(“我稍后会提供详细信息”)。
时间戳依赖
请注意,块的时间戳可以由矿工操纵,并且应考虑时间戳的所有直接和间接使用。 区块数量和平均出块时间可用于估计时间,但这不是区块时间在未来可能改变(例如Casper期望的更改)的证明。
uint someVariable = now + 1; if (now % 2 == 0) { // the now can be manipulated by the miner } if ((someVariable - 100) % 2 == 0) { // someVariable can be manipulated by the miner }
//就是矿工可以通过改变now进而改变someVariable变量
整数上溢和下溢
这里大概有 20个关于上溢和下溢的情况。
https://github.com/ethereum/solidity/issues/796#issuecomment-253578925
考虑如下这个简单的转账操作:
mapping (address => uint256) public balanceOf; // INSECURE function transfer(address _to, uint256 _value) { /* Check if sender has balance */ if (balanceOf[msg.sender] < _value) throw; /* Add and subtract new balances */ balanceOf[msg.sender] -= _value; balanceOf[_to] += _value; } // SECURE function transfer(address _to, uint256 _value) { /* Check if sender has balance and for overflows */ if (balanceOf[msg.sender] < _value || balanceOf[_to] + _value < balanceOf[_to])//防止溢出的方法,或者使用safeMath throw; /* Add and subtract new balances */ balanceOf[msg.sender] -= _value; balanceOf[_to] += _value; }
如果余额到达uint的最大值(2^256),这时候再加1便会导致数字上溢,然后余额就会变为0。应当检查这里。溢出是否与之相关取决于具体的实施方式。想想uint值是否有机会变得这么大或和谁会改变它的值。如果任何用户都有权利更改uint的值,那么它将更容易受到攻击。如果只有管理员能够改变它的值,那么它可能是安全的,因为没有别的办法可以跨越这个限制。
对于下溢同样的道理。如果一个uint被改变后小于0,那么将会导致它下溢并且被设置成为最大值(2^256)。
对于较小数字的类型比如uint8、uint16、uint24等也要小心:他们更加容易达到最大值,所以更容易溢出。
通过(Unexpected) Throw发动DoS
考虑如下简单的智能合约:
// bad contract auction { address highestBidder; uint highestBid; function bid() payable { if (msg.value < highestBid) throw; if (highestBidder != 0) {
//运行到这里就说明有更高竞价者出现了,currentLeader还是旧的竞价者
//然后拿回自己竞价时付给合约的钱 if (!highestBidder.send(highestBid)) { // if this call consistently fails, no one else can bid throw; } } highestBidder = msg.sender; highestBid = msg.value; } }
当有更高竞价时,它将试图退款给曾经最高竞价人,如果退款失败则会抛出异常。
这意味着,恶意投标人可以成为当前最高竞价人,同时确保对其地址的任何退款始终失败,然后无论是任何最高竞价者都会卡在if (!currentLeader.send(highestBid))的对恶意投标人的退款中,永远无法向下走,将自己声明为currentLeader。这样就可以阻止任何人调用“bid()”函数,使自己永远保持领先。建议向之前所说的那样建立基于pull的支付系统 。
外部调用可能会有意或无意的失败。为了最小化这些外部调用失败带来的损失,通常好的做法是将外部调用函数与其余代码隔离,最终是由收款发起方负责发起调用该函数。这种做法对付款操作尤为重要,比如让用户自己撤回资产而不是直接发送给他们。
// good contract auction { address highestBidder; uint highestBid; mapping(address => uint) refunds; function bid() payable external { if (msg.value < highestBid) throw; if (highestBidder != 0) {//创建一个临时账户 refunds[highestBidder] += highestBid; // record the refund that this user can claim } highestBidder = msg.sender; highestBid = msg.value; } function withdrawRefund() external { uint refund = refunds[msg.sender]; refunds[msg.sender] = 0;//先将临时账户清零,以免调用send时起触发fallback回退函数来递归调用withdrawRefund() if (!msg.sender.send(refund)) { refunds[msg.sender] = refund; // reverting state because send failed } } }
另一个例子是合约可能通过数组迭代来向用户支付(例如,众筹合约中的支持者)时。 通常要确保每次付款都成功。 如果没有,应该抛出异常。
问题是,如果其中一个支付失败,您将恢复整个支付系统,这意味着该循环将永远不会完成。 因为一个地址没有转账成功导致其他人都没得到报酬。
address[] private refundAddresses; mapping (address => uint) public refunds; // bad function refundAll() public { for(uint x; x < refundAddresses.length; x++) { // arbitrary length iteration based on how many addresses participated if(!refundAddresses[x].send(refunds[refundAddresses[x]])) { throw; // doubly bad, now a single failure on send will hold up all funds } } }
解决方案,这里我们的建议是使用预授权方式付款(这是什么,没查到,留着后面补?????)。
再一次强调,同样的解决办法: 优先使用pull 而不是push支付系统。
就是也是把两个内容拆开,这样,一个withdraw出错,只会revert一个账户,不会导致所有账户都失败(),我自己写的,不知道对不对:
address[] private refundAddresses; mapping (address => uint) public refunds; function refundAll() public { for(uint x; x < refundAddresses.length; x++) { // arbitrary length iteration based on how many addresses participated uint amount = refunds[refundAddresses[x]]; refunds[refundAddresses[x]] = 0; withdraw(refundAddresses[x],amount); } } function withdraw(address refundAddress,uint amount) public { if (!(refundAddress.send(amount))) { throw; } }
通过区块Gas Limit发动DoS
在先前的例子中你可能已经注意到另一个问题:一次性向所有人转账,很可能会导致达到以太坊区块gas limit的上限。以太坊规定了每一个区块所能花费的gas limit,如果超过你的交易便会失败。
即使没有故意的攻击,这也可能导致问题。然而,最为糟糕的是如果gas的花费被攻击者操控。在先前的例子中,如果攻击者增加一部分收款名单,并设置每一个收款地址都接收少量的退款。这样一来,更多的gas将会被花费从而导致达到区块gas limit的上限,整个转账的操作也会以失败告终。
又一次证明了 优先使用pull 而不是push支付系统。
如果你实在必须通过遍历一个变长数组来进行转账,最好估计完成它们大概需要多少个区块以及多少笔交易。然后你还必须能够追踪得到当前进行到哪以便当操作失败时从那里开始恢复,举个好例子:
struct Payee { address addr; uint256 value; } Payee payees[]; uint256 nextPayeeIndex; function payOut() { uint256 i = nextPayeeIndex; while (i < payees.length && msg.gas > 200000) {//如果gas不足200000了,将会停止,且下一次再开始时能够从上一次停止的地方nextPayeeIndex开始 payees[i].addr.send(payees[i].value); i++; } nextPayeeIndex = i; }
如上所示,你必须确保在下一次执行payOut()
之前另一些正在执行的交易不会发生任何错误。如果必须,请使用上面这种方式来处理。
Call Depth攻击
由于EIP 150 (https://github.com/ethereum/EIPs/issues/150但是有空还是要了解一下??????)进行的硬分叉,Call Depth攻击已经无法实施* (由于以太坊限制了Call Depth最大为1024,确保了在达到最大深度之前gas都能被正确使用)
软件工程开发技巧
正如我们先前在基本理念 章节所讨论的那样,避免自己遭受已知的攻击是不够的。由于在链上遭受攻击损失是巨大的,因此你还必须改变你编写软件的方式来抵御各种攻击。
我们倡导“时刻准备失败",提前知道你的代码是否安全是不可能的。然而,我们可以允许合约以可预知的方式失败,然后最小化失败带来的损失。本章将带你了解如何为可预知的失败做准备。
注意:当你向你的系统添加新的组件时总是伴随着风险的。一个不良设计本身会成为漏洞-一些精心设计的组件在交互过程中同样会出现漏洞。仔细考虑你在合约里使用的每一项技术,以及如何将它们整合共同创建一个稳定可靠的系统。
升级有问题的合约
如果代码中发现了错误或者需要对某些部分做改进都需要更改代码。在以太坊上发现一个错误却没有办法处理他们是太多意义的。
关于如何在以太坊上设计一个合约升级系统是一个正处于积极研究的领域,在这篇文章当中我们没法覆盖所有复杂的领域。然而,这里有两个通用的基本方法:
1)最简单的是专门设计一个注册合约,在注册合约中保存最新版合约的地址。对于合约使用者来说更能实现无缝衔接的方法是设计一个合约,使用它转发调用请求和数据到最新版的合约。
无论采用何种技术,组件之间都要进行模块化和良好的分离,由此代码的更改才不会破坏原有的功能,造成孤儿数据,或者带来巨大的成本。 尤其是将复杂的逻辑与数据存储分开,这样你在使用更改后的功能时不必重新创建所有数据。
当需要多方参与决定升级代码的方式也是至关重要的。根据你的合约,升级代码可能会需要通过单个或多个受信任方参与投票决定。如果这个过程会持续很长时间,你就必须要考虑是否要换成一种更加高效的方式以防止遭受到攻击,例如紧急停止或断路器,在下面说明。
Example 1:使用注册合约存储合约的最新版本
在这个例子中,调用没有被转发,因此用户必须每次在交互之前都先获取最新的合约地址。
contract SomeRegister { address backendContract; address[] previousBackends; address owner; function SomeRegister() { owner = msg.sender; } modifier onlyOwner() { if (msg.sender != owner) { throw; } _; } function changeBackend(address newBackend) public onlyOwner() returns (bool) { if(newBackend != backendContract) { previousBackends.push(backendContract); backendContract = newBackend; return true; } return false; } }
这种方法有两个主要的缺点:
1、用户必须始终查找当前合约地址,否则任何未执行此操作的人都可能会使用旧版本的合约 2、在你替换了合约后你需要仔细考虑如何处理原合约中的数据
2)另外一种方法是设计一个用来转发调用请求和数据到最新版的合约:
例2: 使用DELEGATECALL
转发数据和调用,不明白?????
contract Relay { address public currentVersion; address public owner; modifier onlyOwner() { if (msg.sender != owner) { throw; } _; } function Relay(address initAddr) { currentVersion = initAddr; owner = msg.sender; // this owner may be another contract with multisig, not a single contract owner } function changeContract(address newVersion) public onlyOwner() { currentVersion = newVersion; } function() { if(!currentVersion.delegatecall(msg.data)) throw; } }
这种方法避免了先前的问题,但也有自己的问题。它使得你必须在合约里小心的存储数据。如果新的合约和先前的合约有不同的存储层,你的数据可能会被破坏。另外,这个例子中的模式没法从函数里返回值,只负责转发它们,由此限制了它的适用性。(这里有一个更复杂的实现 想通过内联汇编和返回大小的注册表来解决这个问题)
无论你的方法如何,重要的是要有一些方法来升级你的合约,否则当被发现不可避免的错误时合约将没法使用。
断路器(暂停合约功能)
由于断路器在满足一定条件时将会停止执行,如果发现错误时可以使用断路器。例如,如果发现错误,大多数操作可能会在合约中被挂起,这是唯一的操作就是撤销。你可以授权给任何你受信任的一方,提供给他们触发断路器的能力,或者设计一个在满足某些条件时自动触发某个断路器的程序规则。
例如:
bool private stopped = false; address private owner; modifier isAdmin() { if(msg.sender != owner) { throw; } _; } function toggleContractActive() isAdmin public { // You can add an additional modifier that restricts stopping a contract to be based on another action, such as a vote of users stopped = !stopped; } modifier stopInEmergency { if (!stopped) _; }//就是在某种条件下停止某些操作,cryptopunks中也有用到 modifier onlyInEmergency { if (stopped) _; } function deposit() stopInEmergency public { // some code } function withdraw() onlyInEmergency public { // some code }
速度碰撞(延迟合约动作)
速度碰撞使动作变慢,所以如果发生了恶意操作便有时间恢复。例如,The DAO 从发起分割DAO请求到真正执行动作需要27天。这样保证了资金在此期间被锁定在合约里,增加了系统的可恢复性。在DAO攻击事件中,虽然在速度碰撞给定的时间段内没有有效的措施可以采取,但结合我们其他的技术,它们是非常有效的。
例如:
struct RequestedWithdrawal { uint amount; uint time; } mapping (address => uint) private balances; mapping (address => RequestedWithdrawal) private requestedWithdrawals; uint constant withdrawalWaitPeriod = 28 days; // 4 weeks function requestWithdrawal() public { if (balances[msg.sender] > 0) { uint amountToWithdraw = balances[msg.sender]; balances[msg.sender] = 0; // for simplicity, we withdraw everything; // presumably, the deposit function prevents new deposits when withdrawals are in progress requestedWithdrawals[msg.sender] = RequestedWithdrawal({ amount: amountToWithdraw, time: now//把你现在的时间记下,当过了28 days后才能withdraw }); } } function withdraw() public {
//判断账户是否withdraw过了,以及是否过了28 days if(requestedWithdrawals[msg.sender].amount > 0 && now > requestedWithdrawals[msg.sender].time + withdrawalWaitPeriod) { uint amountToWithdraw = requestedWithdrawals[msg.sender].amount; requestedWithdrawals[msg.sender].amount = 0; if(!msg.sender.send(amountToWithdraw)) { throw; } } }
速率限制
速率限制暂停或需要批准进行实质性更改。 例如,只允许存款人在一段时间内提取总存款的一定数量或百分比(例如,1天内最多100个ether) - 该时间段内的额外提款可能会失败或需要某种特别批准。 或者将速率限制做在合约级别,合约期限内只能发出发送一定数量的代币。
contract CircuitBreaker { struct Transfer { uint amount; address to; uint releaseBlock; bool released; bool stopped; } Transfer[] public transfers; address public curator; address public authorizedSender; uint public period; uint public limit; uint public currentPeriodEnd; uint public currentPeriodAmount; event PendingTransfer(uint id, uint amount, address to, uint releaseBlock); function CircuitBreaker(address _curator, address _authorizedSender, uint _period, uint _limit) { curator = _curator; period = _period; limit = _limit; authorizedSender = _authorizedSender; currentPeriodEnd = block.number + period; } function transfer(uint amount, address to) { if (msg.sender == authorizedSender) { updatePeriod(); if (currentPeriodAmount + amount > limit) { uint releaseBlock = block.number + period; PendingTransfer(transfers.length, amount, to, releaseBlock); transfers.push(Transfer(amount, to, releaseBlock, false, false)); } else { currentPeriodAmount += amount; transfers.push(Transfer(amount, to, block.number, true, false)); if(!to.send(amount)) throw; } } } function updatePeriod() { if (currentPeriodEnd < block.number) { currentPeriodEnd = block.number + period; currentPeriodAmount = 0; } } function releasePendingTransfer(uint id) { Transfer transfer = transfers[id]; if (transfer.releaseBlock <= block.number && !transfer.released && !transfer.stopped) { transfer.released = true; if(!transfer.to.send(transfer.amount)) throw; } } function stopTransfer(uint id) { if (msg.sender == curator) { transfers[id].stopped = true; } } }
合约发布
在将大量资金放入合约之前,合约应当进行大量的长时间的测试。
至少应该:
- 拥有100%测试覆盖率的完整测试套件(或接近它)
- 在自己的testnet上部署
- 在公共测试网上部署大量测试和错误奖励
- 彻底的测试应该允许各种玩家与合约进行大规模互动
- 在主网上部署beta版以限制风险总额
自动弃用
在合约测试期间,你可以在一段时间后强制执行自动弃用以阻止任何操作继续进行。例如,alpha版本的合约工作几周,然后自动关闭所有除最终退出操作的操作。
modifier isActive() { if (block.number > SOME_BLOCK_NUMBER) { throw; } _; } function deposit() public isActive() { // some code } function withdraw() public { // some code }
#####限制每个用户/合约的Ether数量
在早期阶段,你可以限制任何用户(或整个合约)的Ether数量 - 以降低风险。
Bug赏金计划
。。。这部分省略,自己看
安全相关的文件和程序
。。。这部分省略,自己看
安全工具(好好学学怎么用!!!!!然后写一篇文章)
- Oyente - 根据这篇文章分析Ethereum代码以找到常见的漏洞。
- solidity-coverage - Solidity代码覆盖率测试
- Solgraph - 生成一个DOT图,显示了Solidity合约的功能控制流程,并highlight了潜在的安全漏洞。
Linters(好好学学怎么用!!!!!然后写一篇文章)
Linters通过约束代码风格和排版来提高代码质量,使代码更容易阅读和查看。
- Solium - 另一种Solidity linting。
- Solint - 帮助你实施代码一致性约定来避免你合约中的错误的Solidity linting
- Solcheck - 用JS写的Solidity linter,(实现上)深受eslint的影响。
https://blog.ethereum.org
关于以太坊的博客
https://ethstats.net
网络状态页面