玩以太坊链上项目的必备技能(修改器 [modifier]-Solidity之旅十五)
修改器(modifier)
在讲修改器(modifier)
之前,我们使用前面几篇文章所学到的知识来实现一个简单的 token 类合约。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract InheritanceModifierExample {
mapping(address => uint) public tokenBalance;
//拥有者
address owner;
uint tokenPrice = 1 ether;
constructor() {
owner = msg.sender;
tokenBalance[owner] = 100;
}
function createNewToken() public {
//使用 require 检查是不是合约拥有着
require(msg.sender == owner, "You are not allowed");
tokenBalance[owner]++;
}
function burnToken() public {
require(msg.sender == owner, "You are not allowed");
tokenBalance[owner]--;
}
function purchaseToken() public payable {
require((tokenBalance[owner] * tokenPrice) / msg.value > 0, "not enough tokens");
tokenBalance[owner] -= msg.value / tokenPrice;
tokenBalance[msg.sender] += msg.value / tokenPrice;
}
function sendToken(address _to, uint _amount) public {
require(tokenBalance[msg.sender] >= _amount, "Not enough tokens");
tokenBalance[msg.sender] -= _amount;
tokenBalance[_to] += _amount;
}
}
我们拷贝部署合约的 account 地址0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
。
切换成其他 account ,也拷贝出地址 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2
。
查看 account Ether 是否正确,复制 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2
,将其填入 tokenBalance
的输入栏,看看 Ether 是否为1。
我们再来看看该账户 burn with Account
会发生什么样的状况呢?
从上述这个简单的代币类合约,聪慧如您,想必是看出它的不足了。
没错,就是上面合约中的函数都有类似的 require 语句,作为开发者的您会将这些 require 语句抽取到一个函数中,但在 Solidity 却引入了一种特别的函数——修改器(modifier)。
Solidity 修改器(modifier)用来做什么的?
修改器(modifier)
在 Solidity 中是一种特殊类型的函数,用于修改其它函数的行为。例如,开发人员可以使用修改器来检查在允许函数执行之前是否满足某个条件。
修改器(modifier)
与函数类似,因为它们可以接受参数并有一个返回类型。修改器(modifier)
也可以被链在一起,这意味着你可以在一个函数上有多个修改器(modifier)
。
然而,修改器(modifier)
只能修改合约逻辑,不能修改合约的存储,包括结构。修改器(modifier)
减少了开发者必须编写模板代码的数量,并且可以使您的 Solidity 代码更加可读。
多个修改器(modifier)
由空格分隔。修改器(modifier)
的顺序很重要。列表中的第一个修改器(modifier)
将被首先执行,第二个修改器(modifier)
将被第二次应用,以此类推。
例如,如果您有一个修改器(modifier)
检查用户是否经过认证,另一个修改器(modifier)
检查用户是否被授权查看某个资源,那么这些修改器(modifier)
的应用顺序将决定用户是否能够查看该资源。
Solidity 修改器(modifier)
的不同类型
Solidity修改器(modifier)
有四大类:闸门检查、先决条件、过滤器和防止重入攻击。
- 闸门检查
它在允许一个函数执行之前检查某个条件是否为真。
例如,您可能有一个允许用户从他们的账户中取钱的函数,但在函数执行之前,开发者可能想检查用户的账户中是否有足够的钱来进行取款。这种检查被认为是一个门检查修改器
。
另一个闸门检查
的例子是,在允许用户查看某个资源之前,检查用户是否经过认证的函数。
- 先决条件
它为一个函数的执行设置了环境,而不是检查某个条件是否为真。
例如,一个 Solidity 开发者可能会使用一个需要一定数量的Ether
来执行函数。在这种情况下,先决条件将是设置Ether
平衡的函数。
- 滤波器
它检查某个条件是否为真,如果是,则允许函数执行。如果条件不为真,那么该函数将不会执行。
与闸门检查
不同,即使条件为真,也不会自动允许函数执行,而过滤器将允许函数在条件为真时执行。
- 重入式攻击的预防
递归攻击
是一种攻击类型,恶意行为者试图通过递归调用多次执行一个函数,以利用它。
例如,想象一下,您有一个允许用户从他们的账户中提款的函数。一个重入式攻击
者可能会试图多次调用该函数,以提取比他们账户中实际拥有的更多的钱。
为了防止重入式攻击
,您可以使用一个修改器
来检查该函数是否被递归调用。如果是,那么该函数将不会执行。
require 和 修改器(modifier)之间的关系
require
经常与修改器
互换使用,因为它们都允许您在一个函数执行之前检查某个条件是否为真。如果指定的条件不为真,那么编译器就会抛出一个错误。
例如,下面的语句使用 require 关键字,以便只有所有者才能与一个函数进行交互。
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
require 和 修改器(modifier)之间的区别。
1、修改器(modiffier)
可用于为一个函数的执行设置环境(如先决条件
的情况下)
2、require
只能用于检查某个条件是否为真 3、
修改器(modiffier)` 可以被重写
4、要求不能被覆盖
什么是修改器(modiffier)
重写?
virtual
关键字可以用来表示可以在派生合约
中override(重写)
。
例如,一个带有修改器(modifier)
的合约,而派生合约继承自它。
假使,基合约中的修改器(modifier)
被标记了virtual
,那么它便可以在派生合约中被override(重写)
。
继承可以让您扩展合约的属性,在修改器(modifier)
的上下文中,继承允许您添加新的(modifier),或覆盖现有的(modifier)
。
下面的简单实现演示了继承和修改器如何一起工作。
contract A {
modifier X virtual {
}
}
contract B is A {
modifier X override {
}
}
在这个例子中,合约 B 继承了合约 A,两个合约都有一个叫做 X
的修改器。然而,在合约 B 中,该修饰语被标记为override
,这表明它覆盖了合约 A 中的修改器。
如何使用修改器
当使用一个修改器时,您首先需要在合约中定义修改器函数。修改器使用一个特殊的符号_
,只有当修改器的条件得到满足时,才会插入函数体。
下面的合同演示了如何使用一个修改器。
contract Owner {
address public owner = msg.sender;
modifier onlyOwner {
require(msg.sender == owner);
_;
}
modifier costs(uint price) {
if (msg.value >= price) {
_;
}
}
}
在上面的例子中,该合约有两个修改器:onlyOwner
costs
。
第一个修改器检查msg.sender
是否是合约的所有者
,第二个修改器检查msg.value
是否大于或等于某个价格。
这两个修饰符都可以用在合同中的任何函数上。
将 _
放在哪里?
_
被称为合并通配符。它将函数代码与 _
所在的修改器代码合并。
换句话说,函数的主体(修改器所连接的)将被插入修改器的定义中出现 _
的地方。
一个修改器必须在其主体内有 _
才能执行。它是强制性的(如果不是这样的话,Solidity 会抛出一个错误吗)。
您写将 _
符号的地方将决定该函数是在修改器代码之前、之间还是之后执行。
modifier SomethingBefore {
require(/* check something first */);
_; // 继续执行功能
}
modifier SomethingAfter {
_; // 先运行函数
require(/* then check something */)
}
如上面的例子所示,您可以把 _
放在修饰语体的开头、中间或结尾。
在实践中,(特别是在您真正了解修改器的工作原理之前),最安全的使用模式是将 _
放在最后。在这种情况下,修改器的作用是一致的验证检查,所以要先检查一个条件,然后再继续。下面的代码片段显示了这个例子。
function isOkay() public view returns(bool) {
// 做一些验证检查
return true;
}
function isAuthorised(address _user) public view returns(bool) {
// 逻辑检查以及账户授权
return true;
}
modifier OnlyIfOkAndAuthorised {
require(isOkay());
require(isAuthorised(msg.sender));
_;
}
向修改器传递参数
修改器也可以接受参数。像函数一样。
modifier Fee (uint _fee) {
if (msg.value >= _fee) {
_;
}
}
使用上面的例子,您可以确保调用您的一个合约函数的用户(或合约)已经发送了一些Ether
来支付预先要求的费用。
让我们用一个简单的例子来说明,这个合约就像一个金库。
您想确保每个想取出储存在合约金库中的钱的用户都要向合约支付最低2.5%的费用。
一个带有参数的修改器可以模拟这种行为。请看下面的代码。
// SPDX-License-Idendifier: GPL-3.0
pragma solidity ^0.8.0;
contract Vault {
modifier fee(uint _fee) {
if (msg.value != _fee) {
revert("You must pay a fee to withdraw your ethers");
} else {
_;
}
}
function deposit(address _user, uint _amount) external {
// ...
}
function withdraw(uint _amount) external payable fee(0.025 ether) {
// ...
}
}
修改器参数允许使用任意表达式,在这种情况下,所有从函数中可见的符号都在修改器中可见。
在一个函数上应用多个修改器。
多个修改器可以应用到一个函数,您可以这样做。
contract OwnerContract {
address public owner = msg.sender;
uint public creationTime = now;
modifier onlyBy(address _account) {
require(
msg.sender == _account,
"Sender not authorized.
);
_;
}
modifier onlyAfter(uint _time) {
require(
now >= _time,
"Function called too early."
);
_;
}
function disown() public onlyBy(owner) onlyAfter(creationTime + 6 weeks) {
delete owner;
}
}
修改器将按照它们被定义的顺序执行,所以从左到右。所以在上面的例子中,该函数将在运行前检查以下条件。
onlyBy(...) : 调用合同的地址是否为所有者?
onlyAfter(...) : 是否有超过6周的时间是该人的所有者?
带枚举的修改器
如果您的合约持有一个枚举类型的状态变量,您可以通过传递一个可用的选项作为修改器的参数来检查它持有的值。
enum State { Created, Locked, Inactive }
State state;modifier isState(State _expectedState) {
require(state == _expectedState);
_;
}