关于智能合约测试-强制以太币发送攻击

关于智能合约测试-强制以太币发送攻击

强制以太币发送攻击(Forced Ether Sending Attack):具体来说,攻击者通过使用 selfdestruct 函数,将合约自毁并强制发送其持有的以太币到目标合约地址,从而绕过目标合约中的限制和检查。

强制以太币发送攻击的详细说明

  1. 原理
    • 以太坊合约中可以通过 selfdestruct 函数将合约的剩余余额发送到任意地址。
    • 这种发送是不经过任何函数调用的,因此目标合约无法对这种发送进行控制或者阻止。
  2. 过程
    • 攻击者部署一个合约并向其中发送以太币。
    • 攻击者调用这个合约的 selfdestruct 函数,并指定目标合约地址。
    • 合约自毁并强制将余额发送到目标合约。
  3. 危害
    • 目标合约可能设计了严格的资金接收规则,比如必须通过特定的函数存入,但这种攻击能绕过这些规则。
    • 这可能破坏合约的业务逻辑,导致错误状态、拒绝服务或者资金丢失。

代码样例及解决方法:

contract EtherGame {
    
    
    uint public targetAmount = 7 ether;
    address public winner;

    function deposit() public payable {
    
    
        require(msg.value == 1 ether, "You can only send 1 Ether");

        uint balance = address(this).balance;
        require(balance <= targetAmount, "Game is over");

        if (balance == targetAmount) {
    
    
            winner = msg.sender;
        }
    }

    function claimReward() public {
    
    
        require(msg.sender == winner, "Not winner");

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

contract Attack {
    
    
    EtherGame etherGame;

    constructor(EtherGame _etherGame) {
    
    
        etherGame = EtherGame(_etherGame);
    }

    function attack() public payable {
    
    
        address payable addr = payable(address(etherGame));
        selfdestruct(addr);
    }
}

智能合约问题分析及其危害

提供的 EtherGame 合约存在一个关键漏洞。Attack 合约利用 selfdestruct 函数来利用这个漏洞。以下是详细分析:

  1. 漏洞解释
    • Attack 合约中的 selfdestruct 函数强制将 Ether 发送到 EtherGame 合约,而无需调用 deposit 函数。
    • 这使得攻击者可以绕过 require(msg.value == 1 ether)require(balance <= targetAmount) 检查,通过直接发送 Ether 到 EtherGame 合约来破坏游戏逻辑。
  2. 危害
    • 游戏逻辑破坏EtherGame 合约的余额可能超过 targetAmount,而没有正确设置获胜者。
    • 拒绝服务攻击:由于余额超过 targetAmount,未来的存款可能会被拒绝,使游戏无法进行。
    • 资金损失:存款的用户可能会失去他们的资金,因为游戏可能无法达到预期状态来领取奖励。

创建新智能合约以修复问题

为了缓解这些问题,我们需要确保 Ether 只能通过 deposit 函数接收,而不能通过强制转账接收。这可以通过使用 modifier 限制访问和一个回退函数来拒绝任何意外的 Ether 转账来实现。

修复后的 EtherGame 合约:
solidity复制代码// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract EtherGame {
    
    
    uint public targetAmount = 7 ether;
    address public winner;
    bool public gameOver;

    modifier onlyWhenActive() {
    
    
        require(!gameOver, "Game is over");
        _;
    }

    function deposit() public payable onlyWhenActive {
    
    
        require(msg.value == 1 ether, "You can only send 1 Ether");

        uint balance = address(this).balance;
        require(balance <= targetAmount, "Game is over");

        if (balance == targetAmount) {
    
    
            winner = msg.sender;
            gameOver = true;
        }
    }

    function claimReward() public {
    
    
        require(msg.sender == winner, "Not winner");

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

    fallback() external payable {
    
    
        revert("Direct transfer not allowed");
    }

    receive() external payable {
    
    
        revert("Direct transfer not allowed");
    }
}
修复内容说明
  1. onlyWhenActive 修饰符
    • 确保游戏结束后不能再进行存款。
  2. gameOver 布尔标志
    • 跟踪游戏是否结束,以防止进一步的存款。
  3. 回退和接收函数
    • 回退函数和接收函数拒绝任何直接发送到合约的 Ether,从而防止使用 selfdestruct 强制转账。
测试修复后的合约

为了测试合约,我们可以通过一系列交易部署并与其交互:

  1. 部署合约
    • 部署 EtherGame 合约。
  2. 进行有效存款
    • 多个地址可以调用 deposit 函数,每次存入正好 1 ether,直到达到目标金额。
  3. 确保获胜者被正确设置
    • 一旦达到目标金额,检查获胜者是否被正确设置。
  4. 尝试强制转账
    • 部署 Attack 合约并尝试将 Ether 强制发送到 EtherGame 合约。交易应失败。
  5. 领取奖励
    • 获胜者调用 claimReward 函数提取资金。

以下是使用 Solidity 测试框架(如 Truffle 或 Hardhat)的简单测试场景:

javascript复制代码const {
    
     expect } = require("chai");
const {
    
     ethers } = require("hardhat");

describe("EtherGame", function () {
    
    
  let etherGame;
  let owner;
  let addr1;
  let addr2;

  beforeEach(async function () {
    
    
    [owner, addr1, addr2] = await ethers.getSigners();
    const EtherGame = await ethers.getContractFactory("EtherGame");
    etherGame = await EtherGame.deploy();
    await etherGame.deployed();
  });

  it("Should accept valid deposits and set the winner", async function () {
    
    
    await addr1.sendTransaction({
    
     to: etherGame.address, value: ethers.utils.parseEther("1") });
    await addr2.sendTransaction({
    
     to: etherGame.address, value: ethers.utils.parseEther("1") });
    await addr1.sendTransaction({
    
     to: etherGame.address, value: ethers.utils.parseEther("1") });
    await addr2.sendTransaction({
    
     to: etherGame.address, value: ethers.utils.parseEther("1") });
    await addr1.sendTransaction({
    
     to: etherGame.address, value: ethers.utils.parseEther("1") });
    await addr2.sendTransaction({
    
     to: etherGame.address, value: ethers.utils.parseEther("1") });
    await addr1.sendTransaction({
    
     to: etherGame.address, value: ethers.utils.parseEther("1") });

    expect(await etherGame.winner()).to.equal(addr1.address);
  });

  it("Should prevent forced transfers", async function () {
    
    
    const Attack = await ethers.getContractFactory("Attack");
    const attack = await Attack.deploy(etherGame.address);
    await attack.deployed();

    await expect(attack.attack({
    
     value: ethers.utils.parseEther("1") })).to.be.revertedWith("Direct transfer not allowed");
  });

  it("Should allow the winner to claim the reward", async function () {
    
    
    await addr1.sendTransaction({
    
     to: etherGame.address, value: ethers.utils.parseEther("1") });
    await addr2.sendTransaction({
    
     to: etherGame.address, value: ethers.utils.parseEther("1") });
    await addr1.sendTransaction({
    
     to: etherGame.address, value: ethers.utils.parseEther("1") });
    await addr2.sendTransaction({
    
     to: etherGame.address, value: ethers.utils.parseEther("1") });
    await addr1.sendTransaction({
    
     to: etherGame.address, value: ethers.utils.parseEther("1") });
    await addr2.sendTransaction({
    
     to: etherGame.address, value: ethers.utils.parseEther("1") });
    await addr1.sendTransaction({
    
     to: etherGame.address, value: ethers.utils.parseEther("1") });

    const initialBalance = await ethers.provider.getBalance(addr1.address);

    await etherGame.connect(addr1).claimReward();

    const finalBalance = await ethers.provider.getBalance(addr1.address);
    expect(finalBalance).to.be.gt(initialBalance);
  });
});

这个测试套件确保合约按预期运行,防止攻击并正确管理游戏逻辑