Solidity是用于开发以太坊智能合约的最受欢迎的语言之一,因此作为一个想要成为区块链开发人员的我决定开始学习如何使用Solidity开发智能合约。
但是由于找不到我想要的质量的教程(使初学者能够以简洁的方式轻松学习基础知识),所以我决定创建本系列,希望很快将您介绍给Solidity编程。
设置开发环境
我们将使用的IDE是Remix。您需要做的就是点击这个URL。点击进去后,您应该会看到一个类似以下的屏幕:
在左侧,我选择了两个主要组件。第一个图标是文件浏览器,您可以在其中查看所有Solidity文件,第二个图标是插件管理器,从中可以添加新插件。
我们将需要添加两个插件,因此单击插件管理器图标,然后键入“ Solidity编译器”。然后通过单击“激活”将其激活,并执行相同的操作以添加“部署并运行事务”插件。
“Solidity编译器”选项卡将允许您配置编译器参数并编译智能合约,而“部署并运行交易”选项卡将允许您在区块链中部署智能合约并与之交互。图标列表现在应如下所示:
您现在应该做的第一件事是检查“编译器”选项卡中的“自动编译”选项。接下来,我们将配置Remix以使用本地存储中的文件。
您将需要使用以下命令安装remixed:
npm install -g remixd
安装完成后,您可以创建一个文件夹,您将在其中放置Solidity文件,然后运行以下命令告诉Remix使用文件夹中的文件:
remixd -s <shared folder> — remix-ide https://remix.ethereum.org
现在转到Remix的主页,单击“连接到本地主机”,然后在弹出窗口中选择“连接”,您应该在“文件资源管理器”选项卡下看到一个名为localhost的区域。如果展开它,您将看到从本地存储共享的文件夹中包含的所有文件:
恭喜你,现在,您拥有了开始编写第一个智能合约的工具!
接着Remix将为您提供在您的浏览器中运行的沙盒区块链,以使用它来部署您的合同并与之交互。只需确保在“部署并运行”选项卡中选择Javascript VM作为环境即可。
撰写"hello world"合同
让我们看看我们的第一个智能合约:
SPDX许可证标识符
如果源代码可用,则可以更好地建立对智能合约的信任。由于提供源代码始终会涉及版权方面的法律问题,因此Solidity编译器鼓励使用机器可读的SPDX许可证标识符。每个源文件都应以说明其许可证的注释开头。TK(https://solidity.readthedocs.io/en/v0.7.3/layout-of-source-files.html)
版本注释
可以为源文件添加版本注释,以拒绝使用将来的编译器版本进行编译,因为这可能会带来不兼容的更改。
TK(https://solidity.readthedocs.io/en/v0.7.3/layout-of-source-files.html)
具有重大更改的发行版本始终以0.x.0或形式x.0.0。在我们的示例中,可以使用的编译器版本应大于或等于0.7.0且小于0.8.0(使用来添加第二个条件^)。可以为编译器版本指定更复杂的规则。它们遵循npm所使用的相同语法。
前两行应出现在所有智能合约文件中。
为了开始编写智能合约,您需要使用关键字contract 以及合约名称,然后才能将代码放在大括号之间。
接着,让我们创建这个世界问候合同。
在您的共享文件夹中创建一个名为的新文件,HelloWorldContract.sol并复制粘贴先前的代码。如果您选中了自动编译选项,它将自动编译代码。如果没有,请确保检查并编译代码。
现在是时候部署我们的第一个合同并与其进行交互了。
单击“部署并运行事务”选项卡,然后确保在环境中选择了JavaScript VM,并在合同区域中选择了合同名称。完成后,单击“部署”。
现在,您应该在“已部署合同”区域下看到您的合同。
如果展开它,您将看到该helloWorld功能。单击它以发送调用此功能的交易,您将看到hello world打印的字符串。
智能合约的结构
现在让我们看看可以在智能合约中添加哪些内容(下面直接引自https://solidity.readthedocs.io/en/v0.6.7/structure-of-a-contract.html):
状态变量:是其值永久存储在合同存储中的变量。
函数:函数是代码的可执行单元。函数通常在合同内部定义,但也可以在合同外部定义。
功能修饰符:功能修饰符可用于以声明方式修改功能的语义。
事件:事件是与EVM日志记录工具的便捷接口。
结构类型:结构是可以将多个变量组合在一起的自定义类型。
枚举类型:枚举可用于创建具有有限“常量值”集合的自定义类型。
在Solidity中,我们有两种类型的变量:
1.状态变量
这些变量在函数外部声明(例如类的属性),并永久存储在以太坊区块链中,更具体地说存储在存储Merkle Patricia树中,这是形成帐户状态的信息的一部分(这就是为什么我们称其为状态变量)。
以太坊Merkle Patricia树
您可以在本文中找到有关数据存储在以太坊区块链中的更多信息。
状态变量可以在声明时进行初始化,并且具有以下可见性:
private:状态变量仅在定义它的合同中可见。
public:状态变量也可以从定义它的协定外部访问,因为编译器会自动创建一个与该变量同名的getter函数。
internal:状态变量对于定义该合同的合同以及从该合同继承的所有其他合同都是可见的。
可见性说明符在状态变量的类型之后给出,如果未指定,则状态变量将被视为内部变量。
2.局部变量
这些是在函数内部声明的变量,其生存期限于声明它们的作用域。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.7.0;
contract HelloWorldContract {
address owner; // state variable
function helloWorld() external pure returns(string memory){
string memory greeting ="hello world"; // local variable
return greeting;
}
}
与其他任何语言一样,我们有原始类型和复杂类型。当用作函数参数或在赋值中时,原始类型始终按值传递,而数组和结构等复杂类型则按引用传递。
3.原始数据类型
固体基本类型
有关地址类型的更多信息:
地址文字:通过地址校验和测试的十六进制文字,例如0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF是address payable类型。
类型转换:address payable可以隐式转换为simple address,这意味着您可以将应付地址分配给该地址,而无需显式转换。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.7.0;
contract HelloWorldContract {
function transferFund(address payable receiver, uint amount) payable external {
address newAddress = receiver;
receiver.transfer(amount); // OK
newAddress.transfer(amount); // shows an error
}
}
对于整数,整数文字并bytes20使用以下语法,允许显式转换为地址:address(x)。这种转换将产生一个address payable类型。
地址成员:地址类型的两个重要成员是balance,它允许查询地址的余额,和transfer,它允许将以太(以wei为单位)发送到应付款地址。
合同和地址类型:您可以使用我们之前看到的语法将合同显式转换为地址类型:address(x)在这种情况下,其中x是合同的一个实例。address payable如果合同定义了接收或应付款的后备功能(我们将在后面详细讨论功能),否则将产生一个合同address,否则,如果您希望它是可付款的,则必须这样做。使用payable(address(x))。
4.全局可用变量
全局命名空间中始终存在一些特殊变量,这些变量主要用于提供有关区块链的信息。
我们可以将这些变量分为三类:
(1)块属性
(2)邮件属性
(3)交易属性
我们将大量使用的两个属性是msg.sender,可以为我们提供发送者的地址,从而可以存储合同所有者的地址,并msg.value获得随消息发送的金额。
让我们举一个例子,在其中存储合同创建者的地址并进行修改,以HelloWorldContract使如果消息是由创建者发送的,我们将向他们打招呼“ hello Daddy;”。否则,我们用“ hello world”向发件人打招呼。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.7.0;
contract HelloWorldContract {
string greeting;
address owner;
constructor() {
greeting = "hello world";
owner = msg.sender;
}
function sayHello() external view returns(string memory) {
if(msg.sender == owner)
return "hello Daddy";
else
return greeting;
}
}
如果按照我们在上一篇文章中看到的那样部署合同,然后单击sayHell,您将得到“ hello Daddy”,因为您用于部署合同的地址与您用来调用该函数的地址相同。
让我们更改帐户并再次调用该函数。为了更改您的帐户,您需要单击“帐户”部分的下拉列表,然后选择其他地址。
如果您sayHello这次打电话,您应该会得到“ hello world”。
在练习应用所学知识之前,我想提到一下this,Solidity中的关键字引用了当前合同的类型,并且可以明确地转换为地址,正如我们在合同实例中所看到的那样。
我现在将剩下的任务留给您作为练习:
合同的寄信人地址。
合同所有者的退货地址。
发件人的返回地址。
退还合同的余额。
返回合同所有者的余额(仅在发送者是所有者的情况下)。
返回发件人的余额。
数据存储如何在Solidity中工作。
以太坊虚拟机(EVM)
EVM的内部工作原理
当我们安装以太坊客户端时,它附带了EVM,这是专门为运行智能合约而创建的轻量级操作系统。EVM的体系结构基于堆栈计算机的模型,这意味着指令集被设计用于堆栈而不是寄存器。
代码执行开始如下:当交易导致智能合约代码执行时,将实例化EVM,并在EVM的ROM中加载要调用的合约的代码。程序计数器设置为零,从合同帐户的存储中加载存储,存储器全部设置为零,并且所有块和环境变量都已设置。然后代码被执行。
资料位置
现在让我们回到Solidity docs中提到的内存关键字。从0.5.0版本开始,所有复杂的类型必须给它们存储在何处明确的数据位置,有三个数据位置:memory,storage,和calldata。
注意:唯一可以省略数据位置的地方是状态变量,因为状态变量将始终存储在帐户的存储器中。
1。storage
输入的数据storage将永久存储。该存储是键值存储。
中的数据storage写在区块链中(因此它们会改变状态),这就是为什么使用存储非常昂贵的原因。
占用256位插槽将花费20,000瓦斯。
更改一个已经占用的插槽的值将花费5,000瓦斯。
清理存储插槽时(即,将非零字节设置为零),将退还一定量的汽油。
存储将数据保存在256位大小(32字节=字)的字段中。即使未完全占用每个插槽,也会产生成本。
2。memory
memory是一个字节数组,其插槽大小为256位(32字节)。此处仅在函数执行期间存储数据。之后,将其删除。它们不会保存到区块链中。
读或写一个字(256位)需要3汽油。
为了避免给矿工带来太多工作,在进行22次作业后,每次作业的成本开始增加。
3。calldata
calldata是存储函数参数的不可修改的,非持久的区域,其行为基本上类似于memory。
calldata外部函数的参数是必需的,但也可以用于其他变量。
它避免了复制,还确保了数据不能被修改。
带有calldata数据位置的数组和结构也可以从函数中返回,但是不可能分配这种类型。
数据位置和分配行为
如果您不想发生意外行为,那么了解数据位置分配的工作原理非常重要。
在分配之间应用以下规则:
之间的分配storage和memory(或calldata)总是创建一个独立的副本。
从memory到分配memory仅创建引用。这意味着对一个内存变量的更改在引用相同数据的所有其他内存变量中也可见。
从storage本地存储变量的赋值也仅分配一个引用。
所有其他作业storage始终复制。这种情况的示例是分配给状态变量或storage结构类型的局部变量成员,即使局部变量本身只是一个引用也是如此。
让我们使用Remix debugger对其进行更详细的研究。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.7.0;
contract DataLocationTest {
uint[] stateVar = [1,4,5];
function foo() public{
// case 1 : from storage to memory
uint[] memory y = stateVar; // copy the content of stateVar to y
// case 2 : from memory to storage
y[0] = 12;
y[1] = 20;
y[2] = 24;
stateVar = y; // copy the content of y to stateVar
// case 3 : from storage to storage
uint[] storage z = stateVar; // z is a pointer to stateVar
z[0] = 38;
z[1] = 89;
z[2] = 72;
}
}
创建一个新文件,复制上面的代码,然后部署合同。
现在尝试调用该函数foo。您将在控制台中看到事务的详细信息,并且在其旁边有一个调试按钮。点击它。
您现在应该看到如下所示的调试器区域:
要遍历代码,请单击我以红色选择的箭头。
您应该注意到的第一件事是,正如我们在EVM部分中提到storage的stateVar,加载了的内容,当然没有局部变量。
当您跨过时,应该看到该变量y出现在局部变量(Solidity locals)部分中。继续进行下去,您会注意到,为了分配必要的内存空间并从中加载每个单词storage,然后将其复制到中,它需要很多字节码memory。这意味着要支付更多的天然气,因此从 storage到分配memory非常昂贵。
让我们研究第二种情况:从memory到赋值storage。
当您完成存储在其中的副本的修改memory并且想要将更改保存回时,可以使用它 storage。它还消耗大量的气体。如果我们从调试器计算步骤详细信息部分中指示的剩余气体差,则为17,083瓦斯。该操作采用了四个SSTORE操作码:第一个用于存储保持不变的阵列大小(消耗了800个气体),另外三个用于更新阵列的值(每个消耗了5,000个气体)。
现在让我们看一下情况三:从存储到存储的分配。这次将创建一个新的局部变量,并包含与相同的内容stateVar。如果我们看一下代码执行,我们会注意到Solidity所做的就是推入包含数组长度的存储的第一个插槽的地址。对于动态数组,包含数组长度的插槽位置用于计算包含数组数据的插槽位置。
如果现在比较将数据复制到的成本memory,然后将其更新并复制回存储(21,629气体)与直接创建参考并直接更新状态(5,085气体)的成本,那么很明显第二种方法要便宜得多。
但是,如果我们像这样直接更新状态变量,该怎么办:
这也是可能的。但是,如果您要处理映射和嵌套数据类型(我们将在后面介绍),则使用storage指针可以导致代码更具可读性。
下面将重点放在引用类型上,该引用类型应显式指定数据位置,正如我们在前几篇文章中提到的那样。我们还将看到如何定义映射,枚举和常量。
数组
在Solidity中,我们有两种类型的阵列:存储阵列和内存阵列。
1.存储阵列:
这些数组被声明为状态变量,并且可以具有固定长度或动态长度。
可以调整具有动态长度的存储阵列的大小,这意味着它们可以访问push()和pop()方法。
pragma solidity ^0.7.0;
contract A {
uint256[] public numbers;// dynamic length array
address[10] private users; // fixed length array
uint8 users_count;
function addUser(address _user) external {
require(users_count < 10, "number of users is limited to 10");
users[users_count] = _user;
users_count++;
}
function addNumber(uint256 _number) external {
numbers.push(_number);
}
}
2.内存阵列:
这些数组memory以其数据位置声明。它们也可以具有固定长度或动态长度,但是动态大小的内存阵列无法调整大小(即,不能调用push()和pop()方法)。数组的大小必须预先计算。
使用new关键字声明动态大小的内存数组,如下所示:
Type[] memory a = new Type[](size)
pragma solidity ^0.7.0;
contract B {
function createMemArrays() external view {
uint256[20] memory numbers;
numbers[0] = 1;
numbers[1] = 2;
uint256 users_num = numbers.length;
address[users_num] memory users1; // ERROR : expected integer literal
// or constant expression
address[] memory users2 = new address[](users_num);
users2[0] = msg.sender; // OK
users2.push(msg.sender); // ERROR : member push is not available
}
}
这里要提到的另一点是关于何时使用内存数组并编写如下内容:
uint256[] memory array;
array[0] = 1;
您不会收到任何警告,但最终将获得无效的操作码,因为array根据内存中布局的说明,该操作码将指向零插槽,因此切勿将其写入。请记住,在使用数组之前,请务必先对其进行初始化,以便获取有效的地址。
数组切片
数组切片只能与calldata数组一起使用,并写为x[start:end]。切片的第一个元素为x[start],最后一个元素为x[end - 1]。
两个start和end是可选的:start默认为0与end默认为数组的长度。
特殊的动态尺寸阵列
1.byte[] 或者 bytes
这些数组可以保存任意长度的原始字节数据。两者之间的区别在于byte[]遵循数组类型的规则,并且如本部分文档所述, Solidity中的内存数组中的元素始终占据32字节的倍数。这意味着,如果元素的长度小于32字节的倍数,则将对其进行填充,直到其适合所需的大小为止。
在byte数组的情况下,这将浪费每个元素31个字节,而对于bytes或则不然string。我会提醒您,从内存中读取或写入一个字(32字节)会消耗3气,这就是为什么建议使用bytes而不是的原因byte[]。
2.string
string是UTF-8数据的动态数组。与其他语言相反,stringSolidity不提供获取字符串长度或执行两个字符串的串联或比较的功能(需要使用库)。
可以使用将字符串转换为字节数组bytes(<string>)。这将返回字符串的UTF-8表示形式的低级字节。
注意:可以将一个字符编码为一个以上的字节,这意味着字节数组的长度不一定是字符串的长度。
3.字符串与 bytes
该文档的大多数示例都使用bytes32代替string,并且还很明确地使用值类型bytes1来bytes32限制字符串的字节数,因为它便宜得多。
结构
与C和C ++一样,结构允许您定义自己的类型,如下所示:
struct Donation {
uint256 value;
uint256 date;
}
一旦定义了结构,就可以开始将其用作状态变量或在函数中使用。
为了初始化一个结构,我们有两种方法:
1.使用位置参数:
Donation donation = Donation(
msg.value,
block.timestamp
);
2.使用关键字:
Donation donation = Donation(
msg.value,
block.timestamp
);
第二种方法将避免我们必须记住结构成员的顺序,因此它可能比第一种有用。
该结构的成员使用点来访问:
uint256 donationDate = myDonation.date;
“虽然结构本身可以是映射成员的值类型,也可以包含其类型的动态大小的数组,但结构不可能包含其自身类型的成员。这种限制是必要的,因为结构的大小必须是有限的。”
对应关系
您可以将映射视为大量的键/值存储,其中每个可能的键都存在,并且可以使用该键一键设置或检索任何值。
映射声明如下:
mapping( KeyType => ValueType) VariableName
该KeyType可以是任何内置值类型(我们看到的那些部分1),字节或字符串,或任何合约或枚举类型。的ValueType可以是任何类型的,包括映射,数组和结构。
这里要提到的一件事是,映射变量唯一允许的数据位置是storage,这意味着您只能将它们声明为状态变量,存储指针或库函数的参数。
枚举
枚举可让您将自定义类型下的相关值分组,如以下示例所示:
enum Color { green , blue, red }
enum使用以下语法可以访问值:
Color defaultColor = Color.green;
注意:也可以在合同或库定义之外的文件级别上声明枚举。
常量和不可变状态变量
状态变量可以声明为constant或immutable。在这两种情况下,在构造合同之后都无法修改变量。对于constant变量,该值必须在编译时固定,而对于immutable,它仍可以在构造时分配。
编译器不会为这些变量保留存储槽,并且每次出现都会由相应的值替换。
常量使用关键字声明constant:
uint256 constant maxParticipants = 10;
对于不可变状态变量,使用关键字声明它们immutable:
contract C {
address immutable owner = msg.sender;
uint256 immutable maxBalance;
constructor(uint256 _maxBalance){
maxBalance = _maxbalance;
}
}
您可以在本节的文档中找到有关常量和不可变状态变量的更多详细信息。
注意:也可以constant在文件级别定义变量。
删除关键字
我要补充的最后一件事是delete在Solidity中的使用。
它用于将变量设置为其初始值,这意味着该语句的delete a行为如下:
对于整数,它等效于a = 0。
对于数组,它分配长度为零的动态数组或长度相同的静态数组,并将所有元素设置为其初始值。
delete a[x]删除x数组索引处的项目,并保持所有其他元素和数组长度不变。这尤其意味着它在阵列中留有间隙。
对于结构,它将为所有成员重置分配一个结构。
delete 对映射没有影响(因为映射的键可能是任意的,并且通常是未知的)。
练习时间:简单
在本练习中,我们将创建一个用于管理用户的合同。
以下是说明:
创建一个新文件并添加一个名为Crud的合同。
创建一个名为User的结构,其中包含用户的ID和名称。
添加两个状态变量并将其公开:1)动态的用户数组;2)每次创建新用户时将增加的id。
下一步是创建Crud函数,但是由于我没有向您介绍Solidity函数,因此我将为您提供声明函数的语法。在下一篇文章中,我们将对它们进行详细的讨论:
函数function_name(<param_type> <param_name>)<可见性> <状态可变性> [returns(<return_type>)] {...}
可见性可以是:公开,私有,内部,外部。
状态可变性可以是:查看,纯净,应付。
这是您将创建的功能的描述。
1.添加
可见性:公共
状态可变性:空
该函数将用户名作为参数,创建一个具有新ID的User实例(每次添加新用户时ID都会自动递增),并将新创建的用户添加到数组中。
2.阅读
可见性:公共
状态可变性:视图
此函数获取要查找的用户的ID,如果找到则返回用户名,否则返回用户名(有关稍后的异常处理)。
3.更新
可见性:公共
状态可变性:空
此函数将获取用户的ID和新名称,然后在找到相应用户时对其进行更新,或者在不存在该用户的情况下还原该事务。
4.销毁
可见性:公共
状态可变性:空
此函数将用户的ID删除,如果找到,则将其从数组中删除;如果用户不存在,则还原事务。
提示:由于最后三个函数都需要查找用户,因此您将需要创建一个私有函数,该函数将获取用户的ID并在找到时返回其在数组中的索引,以避免重复相同的代码。
课后联系答案:
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
contract Crud {
struct User {
uint256 id;
string name;
}
User[] public users;
uint256 public nextId = 1;
function add(string memory name) public {
User memory user = User({id : nextId, name : name});
users.push(user);
nextId++;
}
function read(uint256 id) public view returns(string memory){
uint256 i = find(id);
return users[i].name;
}
function update(uint256 id, string memory newName) public {
uint256 i = find(id);
users[i].name = newName;
}
function destroy(uint256 id) public {
uint256 i = find(id);
delete users[i];
}
function find(uint256 id) private view returns(uint256){
for(uint256 i = 0; i< users.length; i++) {
if(users[i].id == id)
return i;
}
revert("User not found");
}
}
函数和修饰符,它们将在本文结尾为您提供构建多重签名钱包的所有步骤。将在“练习”部分中看到。
Solidity中的函数具有以下形式,它们可以在外部(自由功能)或合同内部编写:
function function_name(<param_type> <param_name>) <visibility> <state mutability> [returns(<return_type>)]{ ... }
返回变量
函数可以返回任意数量的值作为输出。有两种从函数返回变量的方法:
1.使用返回变量的名称:
function arithmetic(uint _a, uint _b) public pure
returns (uint o_sum, uint o_product)
{
o_sum = _a + _b;
o_product = _a * _b;
}
2.直接在return语句中提供返回值:
function arithmetic(uint _a, uint _b) public pure
returns (uint o_sum, uint o_product)
{
return (_a + _b, _a * _b);
}
使用第二种方法,您可以省略返回变量的名称,而仅指定其类型。
支持的参数和返回类型
为了调用智能合约功能,我们需要使用ABI(应用程序二进制接口)规范来指定要调用的功能并对参数进行编码,这些参数将包含在交易的数据字段中并发送给要执行的以太坊网络(ABI编码也用于事件和返回类型)。
ABI编码器的第一个版本不支持我们在前几篇文章中看到的所有类型,例如,我们无法从函数返回结构,如果尝试这样做,则会出现错误,这就是为什么我们需要使用ABI编码器的版本2,以便通过在文件中包含以下行来使错误消失:pragma abicoder v2;如果您使用的是Solidity版本:0.7.5。对于低于0.7.5的版本,我们需要使用实验版本:pragma experimental ABIEncoderV2;
这是来自Solidity文档版本0.7.5的示例:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >0.7.4;
pragma abicoder v2;
contract Test {
struct S { uint a; uint[] b; T[] c; }
struct T { uint x; uint y; }
function f(S memory, T memory, uint) public pure {}
function g() public pure returns (S memory, T memory, uint) {}
}
在文档的此部分中可以找到受支持的ABI类型的完整列表。
能见度
功能的可见性有四种类型:
私有:限制性最强的函数只能在定义智能合约的地方调用。
内部:可以在定义智能合约的位置调用该函数,也可以从该函数继承的所有智能合约中调用该函数。
外部:只能从智能合约外部调用。(如果要从智能合约中调用它,则必须使用它。)
公开:可以在任何地方拨打。(最宽容的一个)
状态变异
view:声明的函数view只能读取状态,而不能修改状态。
pure:用声明的函数pure既不能读取也不能修改状态。
payable:用声明的函数payable可以接受发送给合约的以太币,如果未指定,该函数将自动拒绝所有发送给合约的以太币。
contract SimpleStorage {
uint256 private data;
function getData() external view returns(uint256) {
return data;
}
function setData(uint256 _data) external {
data = _data;
}
}
交易与通话
用view和pure关键字定义的功能不会改变以太坊区块链的状态,这意味着当您调用这些功能时,您不会向区块链发送任何交易,因为交易被定义为将区块链从一种状态转移到另一种状态的状态转换功能。相反,发生的事情是,您要连接的节点通过检查其自己的区块链版本在本地执行功能代码,并在不向以太坊网络广播任何交易的情况下将结果返回。
在本节中,我们将看到一些可以使用的特殊功能。
访问功能
定义为public的状态变量具有getter函数,该函数由编译器自动创建。该函数与变量具有相同的名称,并具有外部可见性。
contract C {
uint public data;
function x() public returns (uint) {
data = 3; // internal access
return this.data(); // external access
}
}
接收以太功能
合同最多只能具有一项receive功能。该函数不能有参数,不能返回任何东西,并且必须具有external可见性和payable状态可变性。
它在发送Ether且未指定任何功能(空调用数据)的合同的调用上执行。这是在普通以太坊传输(例如,通过.send()或.transfer())上执行的功能。
该函数声明如下:
receive() external payable {
...
}
Fallback功能
合同最多只能具有一项fallback功能。此函数不能有参数,不能返回任何东西,并且必须具有external可见性。如果没有其他函数与给定的函数签名匹配,或者根本没有提供任何数据并且没有接收Ether函数,则在调用合同时执行该命令。
您可以这样声明一个函数,如下所示:
fallback() external [payable]{
...
}
“在没有函数调用的情况下直接接收以太币的合同,send或者transfer没有定义receive函数或应付款回退功能的合同,将抛出一个例外,将以太币送回。” —实体文档
在Remix上自行尝试,创建不带receive或的合约,payable fallback然后向其中发送一些以太币。单击Transact之后,您应该会看到一条类似以下的消息。
消息示例
功能修饰符
当您要在执行函数之前检查某些条件时,需要使用修饰符。例如,如果您要检查发件人是否是合同的所有者,则可以编写以下内容:
function selectWinner() external {
require(msg.sender == owner, "this function is restricted to the owner);
...
}
使用修饰符,我们可以隔离此代码,以便我们可以将其与其他功能复用,我们只需要声明修饰符,如下所示:
modifier onlyOwner(){
require(msg.sender == owner, "this function is restricted to the owner);
_; // will be replaced by the code of the function
}
然后将修饰符名称添加到函数中:
function selectWinner() external onlyOwner {
...
}
通过在以空格分隔的列表中指定多个修饰符,可以将它们应用到一个函数,并按给出的顺序对其进行评估。
练习:Multisig钱包
在本练习中,我们将为多签名钱包构建一个智能合约,签名钱包是需要多个密钥才能授权交易的钱包。
我们需要的第一件事是批准者列表和授权交易所需的法定人数(所需的最小用户数,如果我们有3个multisig钱包中的2个,这意味着法定人数为2个)。
您还需要创建一个结构来记录与转账相关的信息,包括要支付的金额,收件人,已经批准转账的批准人数量及其状态(如果已发送或仍在等待确认)批准者)。
过程如下:一名批准者将创建转移,该转移将保存在智能合约的存储中,等待其他批准者确认,一旦达到所需的确认数量,则将以太转移到接收者。
练习答案
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.8.0;
pragma experimental ABIEncoderV2;
contract Wallet {
address[] public approvers;
uint8 public quorum;
struct Transfer {
uint id;
uint amount;
address payable to;
uint approvers;
bool sent;
}
Transfer[] public transfers;
mapping(address => mapping(uint => bool )) public approvals;
constructor(address[] memory _approvers, uint8 _quorum){
approvers = _approvers;
quorum = _quorum;
}
function getApprovers() external view returns(address[] memory){
return approvers;
}
function createTransfer(uint amount, address payable to) external onlyApprover {
transfers.push(Transfer(
transfers.length,
amount,
to,
0,
false
));
}
function getTransfers() external view returns(Transfer[] memory) {
return transfers;
}
function approveTransfer(uint id) external onlyApprover {
require(transfers[id].sent == false, "transfer has already been sent");
require(approvals[msg.sender][id] == false, "cannot approve transfer twice");
approvals[msg.sender][id] = true;
transfers[id].approvers++;
if(transfers[id].approvers >= quorum ) {
transfers[id].sent = true;
address payable to = transfers[id].to;
uint amount = transfers[id].amount;
to.transfer(amount);
}
}
receive() external payable {}
modifier onlyApprover() {
bool isApprover = false;
for(uint8 i=0; i< approvers.length ; i++){
if(approvers[i] == msg.sender){
isApprover = true;
break;
}
}
require(isApprover, "access restricted only to an approver");
_;
}
}
声明:本网站所有相关资料如有侵权请联系站长删除,资料仅供用户学习及研究之用,不构成任何投资建议!