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(), 那么这里潜在的存在上面的攻击,所以你必须认识到任何调用了不受信任的合约代码的合约也是不受信任的。继续浏览下面的相关潜在威胁解决办法的讨论。
跨函数竞态
攻击者也可以使用两个共享状态变量的不同的函数来进行类似攻击。
// INSECURE mapping (address => uint) private userBalances;
function transfer(address to, uint amount) {
if (userBalances[msg.sender] >= amount) { userBalances[to] += amount; userBalances[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 transfer() userBalances[msg.sender] = 0;
}
这个例子中,攻击者在他们外部调用withdrawBalance函数时调用transfer(),如果这个时候withdrawBalance还没有执行到userBalances[msg.sender] = 0;这里,那么他们的余额就没有被清零,那么他们就能够调用transfer()转走代币尽管他们其实已经收到了代币。这个弱点也可以被用到对DAO的攻击。
同样的解决办法也会管用,在执行转账操作之前先清零。也要注意在这个例子中所有函数都是在同一个合约内。然而,如果这些合约共享了状态,同样的bug也可以发生在跨合约调用中。
竞态解决办法中的陷阱
由于竞态既可以发生在跨函数调用,也可以发生在跨合约调用,任何只是避免重入的解决办法都是不够的。
作为替代,我们建议首先应该完成所有内部的工作然后再执行外部调用。这个规则可以避免竞态发生。然而,你不仅应该避免过早调用外部函数而且应该避免调用那些也调用了外部函数的外部函数。例如,下面的这段代码是不安全的:
// INSECURE mapping (address => uint) private userBalances;
mapping (address => bool) private claimedBonus;
mapping (address => uint) private rewardsForA;
functionwithdraw(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; withdraw(recipient); // At this point, the caller will be able to execute getFirstWithdrawalBonus again. claimedBonus[recipient] = true;
}
尽管getFirstWithdrawalBonus() 没有直接调用外部合约,但是它调用的withdraw() 却会导致竞态的产生。在这里你不应该认为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 }
除了修复bug让重入不可能成功,不受信任的函数也已经被标记出来 。
同样的情景:untrustedGetFirstWithdrawalBonus()调用untrustedWithdraw(), 而后者调用了外部合约,因此在这里untrustedGetFirstWithdrawalBonus() 是不安全的。