常见模式

从合约中提取资金

推荐的发送资金方法是在产生影响后使用提取模式。虽然最直观的发送以太币方法是在产生影响后直接使用 transfer 调用,但并不建议这样做,因为它会带来潜在的安全风险。您可以在 安全注意事项 页面了解更多信息。

以下是在合约中实际使用提取模式的示例,目标是将大部分补偿(例如以太币)发送到合约以成为“最富有者”,灵感来自 King of the Ether

在以下合约中,如果您不再是最富有者,您将收到现在是最富有者的资金。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;

contract WithdrawalContract {
    address public richest;
    uint public mostSent;

    mapping(address => uint) pendingWithdrawals;

    /// The amount of Ether sent was not higher than
    /// the currently highest amount.
    error NotEnoughEther();

    constructor() payable {
        richest = msg.sender;
        mostSent = msg.value;
    }

    function becomeRichest() public payable {
        if (msg.value <= mostSent) revert NotEnoughEther();
        pendingWithdrawals[richest] += msg.value;
        richest = msg.sender;
        mostSent = msg.value;
    }

    function withdraw() public {
        uint amount = pendingWithdrawals[msg.sender];
        // Remember to zero the pending refund before
        // sending to prevent reentrancy attacks
        pendingWithdrawals[msg.sender] = 0;
        payable(msg.sender).transfer(amount);
    }
}

这与更直观的发送模式形成对比

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;

contract SendContract {
    address payable public richest;
    uint public mostSent;

    /// The amount of Ether sent was not higher than
    /// the currently highest amount.
    error NotEnoughEther();

    constructor() payable {
        richest = payable(msg.sender);
        mostSent = msg.value;
    }

    function becomeRichest() public payable {
        if (msg.value <= mostSent) revert NotEnoughEther();
        // This line can cause problems (explained below).
        richest.transfer(msg.value);
        richest = payable(msg.sender);
        mostSent = msg.value;
    }
}

请注意,在这个示例中,攻击者可以通过使 richest 成为具有接收或回退函数的合约的地址来使合约陷入不可用状态,该函数会失败(例如,使用 revert() 或仅仅消耗超过转给他们的 2300 gas 津贴)。这样,只要调用 transfer 来将资金交付给“中毒”合约,它就会失败,因此 becomeRichest 也会失败,合约将永远卡住。

相反,如果您使用第一个示例中的“提取”模式,攻击者只能导致他们自己的提取失败,而不会影响合约的其他工作。

限制访问

限制访问是合约中的一种常见模式。请注意,您永远无法阻止任何人或任何计算机读取您的交易内容或合约状态。您可以使用加密使其更难,但如果您的合约应该读取数据,那么其他每个人也都可以读取。

您可以限制 **其他合约** 读取您合约状态的访问权限。实际上,这是默认行为,除非您将状态变量声明为 public

此外,您可以限制谁可以修改您合约的状态或调用您合约的函数,这就是本节的主题。

使用 **函数修饰符** 使这些限制高度可读。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;

contract AccessRestriction {
    // These will be assigned at the construction
    // phase, where `msg.sender` is the account
    // creating this contract.
    address public owner = msg.sender;
    uint public creationTime = block.timestamp;

    // Now follows a list of errors that
    // this contract can generate together
    // with a textual explanation in special
    // comments.

    /// Sender not authorized for this
    /// operation.
    error Unauthorized();

    /// Function called too early.
    error TooEarly();

    /// Not enough Ether sent with function call.
    error NotEnoughEther();

    // Modifiers can be used to change
    // the body of a function.
    // If this modifier is used, it will
    // prepend a check that only passes
    // if the function is called from
    // a certain address.
    modifier onlyBy(address account)
    {
        if (msg.sender != account)
            revert Unauthorized();
        // Do not forget the "_;"! It will
        // be replaced by the actual function
        // body when the modifier is used.
        _;
    }

    /// Make `newOwner` the new owner of this
    /// contract.
    function changeOwner(address newOwner)
        public
        onlyBy(owner)
    {
        owner = newOwner;
    }

    modifier onlyAfter(uint time) {
        if (block.timestamp < time)
            revert TooEarly();
        _;
    }

    /// Erase ownership information.
    /// May only be called 6 weeks after
    /// the contract has been created.
    function disown()
        public
        onlyBy(owner)
        onlyAfter(creationTime + 6 weeks)
    {
        delete owner;
    }

    // This modifier requires a certain
    // fee being associated with a function call.
    // If the caller sent too much, he or she is
    // refunded, but only after the function body.
    // This was dangerous before Solidity version 0.4.0,
    // where it was possible to skip the part after `_;`.
    modifier costs(uint amount) {
        if (msg.value < amount)
            revert NotEnoughEther();

        _;
        if (msg.value > amount)
            payable(msg.sender).transfer(msg.value - amount);
    }

    function forceOwnerChange(address newOwner)
        public
        payable
        costs(200 ether)
    {
        owner = newOwner;
        // just some example condition
        if (uint160(owner) & 0 == 1)
            // This did not refund for Solidity
            // before version 0.4.0.
            return;
        // refund overpaid fees
    }
}

在下一个示例中,我们将讨论一种更专门的限制对函数调用的访问权限的方法。

状态机

合约通常充当状态机,这意味着它们具有某些 **阶段**,在这些阶段它们的行为不同,或者可以调用不同的函数。函数调用通常会结束一个阶段并将合约转换到下一个阶段(特别是如果合约模拟 **交互**)。在某些情况下,某些阶段也会在特定的 **时间点** 自动到达。

盲拍合约就是一个例子,它从“接受盲拍出价”阶段开始,然后过渡到“揭示出价”阶段,最后以“确定拍品结果”阶段结束。

函数修饰符可以用于这种情况来模拟状态并防止对合约的错误使用。

示例

在以下示例中,修饰符 atStage 确保函数只能在特定阶段被调用。

自动定时转换由修饰符 timedTransitions 处理,它应该用于所有函数。

注意

**修饰符顺序很重要**。如果将 atStage 与 timedTransitions 结合使用,请确保在后者之后提到它,这样新阶段就会被考虑在内。

最后,修饰符 transitionNext 可用于在函数结束时自动进入下一阶段。

注意

**修饰符可以跳过**。这仅适用于 0.4.0 之前的 Solidity 版本:由于修饰符是通过简单地替换代码而不是使用函数调用来应用的,因此如果函数本身使用 return,则 transitionNext 修饰符中的代码可以跳过。如果您想这样做,请确保从这些函数手动调用 nextStage。从 0.4.0 版本开始,即使函数明确返回,修饰符代码也会运行。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;

contract StateMachine {
    enum Stages {
        AcceptingBlindedBids,
        RevealBids,
        AnotherStage,
        AreWeDoneYet,
        Finished
    }
    /// Function cannot be called at this time.
    error FunctionInvalidAtThisStage();

    // This is the current stage.
    Stages public stage = Stages.AcceptingBlindedBids;

    uint public creationTime = block.timestamp;

    modifier atStage(Stages stage_) {
        if (stage != stage_)
            revert FunctionInvalidAtThisStage();
        _;
    }

    function nextStage() internal {
        stage = Stages(uint(stage) + 1);
    }

    // Perform timed transitions. Be sure to mention
    // this modifier first, otherwise the guards
    // will not take the new stage into account.
    modifier timedTransitions() {
        if (stage == Stages.AcceptingBlindedBids &&
                    block.timestamp >= creationTime + 10 days)
            nextStage();
        if (stage == Stages.RevealBids &&
                block.timestamp >= creationTime + 12 days)
            nextStage();
        // The other stages transition by transaction
        _;
    }

    // Order of the modifiers matters here!
    function bid()
        public
        payable
        timedTransitions
        atStage(Stages.AcceptingBlindedBids)
    {
        // We will not implement that here
    }

    function reveal()
        public
        timedTransitions
        atStage(Stages.RevealBids)
    {
    }

    // This modifier goes to the next stage
    // after the function is done.
    modifier transitionNext()
    {
        _;
        nextStage();
    }

    function g()
        public
        timedTransitions
        atStage(Stages.AnotherStage)
        transitionNext
    {
    }

    function h()
        public
        timedTransitions
        atStage(Stages.AreWeDoneYet)
        transitionNext
    {
    }

    function i()
        public
        timedTransitions
        atStage(Stages.Finished)
    {
    }
}