关于智能合约测试-强制以太币发送攻击
强制以太币发送攻击(Forced Ether Sending Attack):具体来说,攻击者通过使用 selfdestruct
函数,将合约自毁并强制发送其持有的以太币到目标合约地址,从而绕过目标合约中的限制和检查。
强制以太币发送攻击的详细说明
- 原理:
- 以太坊合约中可以通过
selfdestruct
函数将合约的剩余余额发送到任意地址。 - 这种发送是不经过任何函数调用的,因此目标合约无法对这种发送进行控制或者阻止。
- 以太坊合约中可以通过
- 过程:
- 攻击者部署一个合约并向其中发送以太币。
- 攻击者调用这个合约的
selfdestruct
函数,并指定目标合约地址。 - 合约自毁并强制将余额发送到目标合约。
- 危害:
- 目标合约可能设计了严格的资金接收规则,比如必须通过特定的函数存入,但这种攻击能绕过这些规则。
- 这可能破坏合约的业务逻辑,导致错误状态、拒绝服务或者资金丢失。
代码样例及解决方法:
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
函数来利用这个漏洞。以下是详细分析:
- 漏洞解释:
Attack
合约中的selfdestruct
函数强制将 Ether 发送到EtherGame
合约,而无需调用deposit
函数。- 这使得攻击者可以绕过
require(msg.value == 1 ether)
和require(balance <= targetAmount)
检查,通过直接发送 Ether 到EtherGame
合约来破坏游戏逻辑。
- 危害:
- 游戏逻辑破坏:
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");
}
}
修复内容说明
onlyWhenActive
修饰符:- 确保游戏结束后不能再进行存款。
gameOver
布尔标志:- 跟踪游戏是否结束,以防止进一步的存款。
- 回退和接收函数:
- 回退函数和接收函数拒绝任何直接发送到合约的 Ether,从而防止使用
selfdestruct
强制转账。
- 回退函数和接收函数拒绝任何直接发送到合约的 Ether,从而防止使用
测试修复后的合约
为了测试合约,我们可以通过一系列交易部署并与其交互:
- 部署合约:
- 部署
EtherGame
合约。
- 部署
- 进行有效存款:
- 多个地址可以调用
deposit
函数,每次存入正好 1 ether,直到达到目标金额。
- 多个地址可以调用
- 确保获胜者被正确设置:
- 一旦达到目标金额,检查获胜者是否被正确设置。
- 尝试强制转账:
- 部署
Attack
合约并尝试将 Ether 强制发送到EtherGame
合约。交易应失败。
- 部署
- 领取奖励:
- 获胜者调用
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);
});
});
这个测试套件确保合约按预期运行,防止攻击并正确管理游戏逻辑