美文网首页合约安全
合约安全: selfdestruct自毁函数

合约安全: selfdestruct自毁函数

作者: 梁帆 | 来源:发表于2022-11-21 17:23 被阅读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");
        }
    }
    

    相关文章

      网友评论

        本文标题:合约安全: selfdestruct自毁函数

        本文链接:https://www.haomeiwen.com/subject/mudnxdtx.html