通用模式
从合约中提现
在效果发生后发送资金的推荐方法是使用提现模式。尽管由于效果,发送以太币的最直观方法是直接调用 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;
/// 发送的以太币数量不高于
/// 当前最高金额。
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];
// 记得在发送之前将待退款金额置零
// 以防止重入攻击
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;
/// 发送的以太币数量不高于
/// 当前最高金额。
error NotEnoughEther();
constructor() payable {
richest = payable(msg.sender);
mostSent = msg.value;
}
function becomeRichest() public payable {
if (msg.value <= mostSent) revert NotEnoughEther();
// 这一行可能会导致问题(下面会解释)。
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 {
// 这些将在构造阶段分配,
// 其中 `msg.sender` 是创建此合约的账户。
address public owner = msg.sender;
uint public creationTime = block.timestamp;
// 现在列出此合约可以生成的错误
// 以及特殊注释中的文本说明。
/// 发送者未被授权进行此操作。
error Unauthorized();
/// 函数过早调用
error TooEarly();
/// 函数调用时发送的以太币不足。
error NotEnoughEther();
// 修改器可用于更改函数的主体。
// 如果使用此修改器,
// 它将在函数从某个地址调用时通过检查。
modifier onlyBy(address account)
{
if (msg.sender != account)
revert Unauthorized();
// 不要忘记 "_;"!
// 它将在使用修改器时替换为实际的函数主体。
_;
}
/// 将 `newOwner` 设置为此
/// 合约的新所有者。
function changeOwner(address newOwner)
public
onlyBy(owner)
{
owner = newOwner;
}
modifier onlyAfter(uint time) {
if (block.timestamp < time)
revert TooEarly();
_;
}
/// 删除所有权信息。
/// 只能在合约创建后 6 周调用。
function disown()
public
onlyBy(owner)
onlyAfter(creationTime + 6 weeks)
{
delete owner;
}
// 此修改器要求与函数调用相关联的特定费用 。
// 如果调用者发送的金额过多,他或她将被退款,但仅在函数主体之后。
// 在 Solidity 版本 0.4.0 之前,这很危险,
// 因为可以跳过 `_;` 后面的部分。
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;
// 只是一些示例条件
if (uint160(owner) & 0 == 1)
// 这在 Solidity 0.4.0 版本之前没有退款。
return;
// 退款多支付的费用
}
}
在下一个示例中,将讨论限制对函数调用的访问的更专业方法。
状态机
合约通常充当状态机,这意味着它们具有某些 阶段,在这些阶段中它们的行为不同,或者可以调用不同的函数。函数调用通常结束一个阶段,并将合约转换到下一个阶段(特别是当合约模拟 交互 时)。某些阶段在 时间 的某个点上也会自动到达。
一个例子是盲拍合约,它从“接受盲标”阶段开始,然后过渡到“揭示标”阶段,最后以“确定拍卖结果”结束。
在这种情况下,可以使用函数修改器来建模状态并防止合约的错误使用。
示例
在以下示例中,修改器 atStage
确保该函数只能在特定阶段调用。
自动定时转换由修改器 timedTransitions
处理,应该在所有函数中使用。
备注
修改器的顺序很重要。 如果 atStage 与 timedTransitions 结合使用,请确保在后者之后提及它,以便新阶段被考虑在内。
最后,修改器 transitionNext
可以在函数完成时自动进入下一个阶段。
备注
修改器可以被跳过。 这仅适用于 0.4.0 版本之前的 Solidity: 由于修改器是通过简单替换代码而不是通过函数调用来应用的, 如果函数本身使用返回,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
}
/// 函数在此时无法调用。
error FunctionInvalidAtThisStage();
// 这是当前阶段。
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);
}
// 执行定时转换。确保首先提及
// 此修改器,否则保护措施
// 将不会考虑新阶段。
modifier timedTransitions() {
if (stage == Stages.AcceptingBlindedBids &&
block.timestamp >= creationTime + 10 days)
nextStage();
if (stage == Stages.RevealBids &&
block.timestamp >= creationTime + 12 days)
nextStage();
// 其他阶段通过交易转换
_;
}
// 修改器的顺序在这里很重要!
function bid()
public
payable
timedTransitions
atStage(Stages.AcceptingBlindedBids)
{
// 我们在这里不会实现
}
function reveal()
public
timedTransitions
atStage(Stages.RevealBids)
{
}
// 此修改器在函数完成后
// 进入下一个阶段。
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)
{
}
}