重⼊攻击,可能是最着名的以太坊漏洞。合约在开发过程中,使用了危险的函数,并且使用不安全的交互模式。两者叠加在一起造就了以太坊非常经典的重入漏洞。其中最有代表性的攻击是 The DAO
原理分析
外部恶意合约回调了受攻击合约上的一个函数,并在受攻击合约上的任意位置“重新进入”代码执行。因为原合约的程序员可能没有预料到合约代码可以被”重入“,因此合约会出现不可预知的行为。在 gas 足够的情况下,合约之间甚至可以相互循环调用,直至达到 gas 的上限,但是如果循环中有转账之类的操作,就会导致严重的后果。
1 2 3 4 5
function withdraw(uint _amount) public { require(balances[msg.sender] >= _amount) msg.sender.call.value(_amount)(); balances[msg.sender] -= _amount; }
// SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.3.2 (security/ReentrancyGuard.sol)
pragma solidity ^0.8.0;
/** * @dev Contract module that helps prevent reentrant calls to a function. * * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier * available, which can be applied to functions to make sure there are no nested * (reentrant) calls to them. * * Note that because there is a single `nonReentrant` guard, functions marked as * `nonReentrant` may not call one another. This can be worked around by making * those functions `private`, and then adding `external` `nonReentrant` entry * points to them. * * TIP: If you would like to learn more about reentrancy and alternative ways * to protect against it, check out our blog post * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]. */ abstract contract ReentrancyGuard { // Booleans are more expensive than uint256 or any type that takes up a full // word because each write operation emits an extra SLOAD to first read the // slot's contents, replace the bits taken up by the boolean, and then write // back. This is the compiler's defense against contract upgrades and // pointer aliasing, and it cannot be disabled.
// The values being non-zero value makes deployment a bit more expensive, // but in exchange the refund on every call to nonReentrant will be lower in // amount. Since refunds are capped to a percentage of the total // transaction's gas, it is best to keep them low in cases like this one, to // increase the likelihood of the full refund coming into effect. uint256 private constant _NOT_ENTERED = 1; uint256 private constant _ENTERED = 2;
uint256 private _status;
constructor() { _status = _NOT_ENTERED; }
/** * @dev Prevents a contract from calling itself, directly or indirectly. * Calling a `nonReentrant` function from another `nonReentrant` * function is not supported. It is possible to prevent this from happening * by making the `nonReentrant` function external, and making it call a * `private` function that does the actual work. */ modifier nonReentrant() { // On the first call to nonReentrant, _notEntered will be true require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
// Any calls to nonReentrant after this point will fail _status = _ENTERED;
_;
// By storing the original value once again, a refund is triggered (see // https://eips.ethereum.org/EIPS/eip-2200) _status = _NOT_ENTERED; } }
// SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.3.2 (security/PullPayment.sol)
pragma solidity ^0.8.0;
import "../utils/escrow/Escrow.sol";
/** * @dev Simple implementation of a * https://consensys.github.io/smart-contract-best-practices/recommendations/#favor-pull-over-push-for-external-calls[pull-payment] * strategy, where the paying contract doesn't interact directly with the * receiver account, which must withdraw its payments itself. * * Pull-payments are often considered the best practice when it comes to sending * Ether, security-wise. It prevents recipients from blocking execution, and * eliminates reentrancy concerns. * * TIP: If you would like to learn more about reentrancy and alternative ways * to protect against it, check out our blog post * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]. * * To use, derive from the `PullPayment` contract, and use {_asyncTransfer} * instead of Solidity's `transfer` function. Payees can query their due * payments with {payments}, and retrieve them with {withdrawPayments}. */ abstract contract PullPayment { Escrow private immutable _escrow;
constructor() { _escrow = new Escrow(); }
/** * @dev Withdraw accumulated payments, forwarding all gas to the recipient. * * Note that _any_ account can call this function, not just the `payee`. * This means that contracts unaware of the `PullPayment` protocol can still * receive funds this way, by having a separate account call * {withdrawPayments}. * * WARNING: Forwarding all gas opens the door to reentrancy vulnerabilities. * Make sure you trust the recipient, or are either following the * checks-effects-interactions pattern or using {ReentrancyGuard}. * * @param payee Whose payments will be withdrawn. */ function withdrawPayments(address payable payee) public virtual { _escrow.withdraw(payee); }
/** * @dev Returns the payments 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. * Funds sent in this way are stored in an intermediate {Escrow} contract, so * there is no danger of them being spent before withdrawal. * * @param dest The destination address of the funds. * @param amount The amount to transfer. */ function _asyncTransfer(address dest, uint256 amount) internal virtual { _escrow.deposit{value: amount}(dest); } }