常见模式
从合约中提取资金
推荐的发送资金方法是在产生影响后使用提取模式。虽然最直观的发送以太币方法是在产生影响后直接使用 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)
{
}
}