01-入门智能合约

前言

之前使用web3js调用了很多合约进行交互,那么如果想深入了解的话,还是需要学习一下合约相关的知识,这是一篇入门笔记,能让我们对合约有个大概的认知。

通过这篇文章你能了解到:

  • 如何存储数据上以太坊链
  • 如何发行一个自己的Token(币)
  • 实现代币的增发
  • 实现代币余额的查询
  • 实现代币的转账

准备工作

可以使用remix编辑器和solidity文档

简单的智能合约

存储链上数据

目标:

  • 能在EVM(以太坊虚拟机)存储一个数据
  • 能查询该数据
1
2
3
4
5
6
7
8
9
10
11
12
13
pragma solidity ^0.4.0;

contract SimpleStorage {
uint storedData;

function set(uint x) public {
storedData = x;
}

function get() public view returns (uint) {
return storedData;
}
}

把代码复制进remix编辑器,点击 complite进行编译 👇

image-20220115201618121

编译成功之后,需要将代码部署。切换点击deploy进行部署 👇

image-20220115201639026

部署成功之后就会看到自己刚刚写的测试方法,有一个set方法,需要传入一个unit256类型的数据。

同时还有一个get方法

image-20220115201831778

注意这里按钮的颜色,set是黄色,get是蓝色。

蓝色代表查询,不需要付gas费用,而黄色set代表设置,对链上数据进行更改,就需要付gas费用

我们可以直接点击get,就可以直接调用该方法。

image-20220115202017501

就可以看到控制台会输出这些信息,在decode output里面可以看到这个数字现在默认为0

然后我们使用set调用一下该方法:

image-20220115202122950

如图所示,我将一个数字为77设置给了这个存储storedData。然后需要花费gas,最后数字也变成了77,再次调用get,就能看到该数字为77了

image-20220115202216994

至此,我们基本上了解了合约简单的编写,到编译,再到部署和调用的整个过程了。

发一个币

目标:

  • 我们可以无限量发币
  • 只有合约创建者可以发币
  • 币可以发送给别人
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;

contract Coin {
// 关键字“public”生成变量,允许外部来访问该变量
// 可从其他合约访问minter
address public minter;
mapping (address => uint) public balances;

// 以通过事件针对变化作出高效的反应
// 会在send方法中调用
event Sent(address from, address to, uint amount);

// 构造函数,在合约创建的时候运行一次
constructor() {
minter = msg.sender;
}

// 将一定数量的新创建的硬币发送到一个地址
// 只能被合约创建者调用
function mint(address receiver, uint amount) public {
require(msg.sender == minter);
balances[receiver] += amount;
}

// 错误提醒声明
error InsufficientBalance(uint requested, uint available);

// 将一个币发送到另外一个地址
function send(address receiver, uint amount) public {
if (amount > balances[msg.sender])
revert InsufficientBalance({
requested: amount,
available: balances[msg.sender]
});

balances[msg.sender] -= amount;
balances[receiver] += amount;
emit Sent(msg.sender, receiver, amount);
}
}

备注说明

基本上代码上都写了注释,但是有几个特别的地方。

一个是mint()方法中的中的 require,一个是 send()方法中的revert这两处不是很明白。

错误处理:Assert, Require, Revert and Exceptions

便利函数 assertrequire 可用于检查条件并在条件不满足时抛出异常。assert 函数只能用于测试内部错误,并检查非变量。 require 函数用于确认条件有效性,例如输入变量,或合约状态变量是否满足条件,或验证外部合约调用返回的值。 如果使用得当,分析工具可以评估你的合约,并标示出那些会使 assert 失败的条件和函数调用。 正常工作的代码不会导致一个 assert 语句的失败;如果这发生了,那就说明出现了一个需要你修复的 bug。

还有另外两种触发异常的方法:revert 函数可以用来标记错误并恢复当前的调用。 revert 调用中包含有关错误的详细信息是可能的,这个消息会被返回给调用者。已经不推荐的关键字 throw 也可以用来替代 revert()

案例解释

require 等同于 if throw

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.4.22;

contract VendingMachine {
function buy(uint amount) payable {
if (amount > msg.value / 2 ether)
revert("Not enough Ether provided.");
// 下边是等价的方法来做同样的检查:
require(
amount <= msg.value / 2 ether,
"Not enough Ether provided."
);
// 执行购买操作
}
}

同样作为判断一个条件是否满足的函数,require会退回剩下的gas,而assert会烧掉所有的gas

编译部署

按照上面方法,我们进行compile 和 deploy

成功之后就会看到这些方法部署了。

image-20220115210911122

我们是用0x5B38Da6a701c568545dCfcB03FcB875f56beddC4这个账号部署的,所以先用blances()方法来看下有多少代币余额。

image-20220115211144288

没有任何余额。

然后通过mint我们发行一些余额。

mint()方法需要2个参数分别是接受的地址和数量

我直接用发行合约的地址0x5B38Da6a701c568545dCfcB03FcB875f56beddC4以及传入1000000000000000000wei,也就是1eth

image-20220115211303252

然后再次调用balances()方法查看我们该账户的余额,就可以看到有1000000000000000000这么多个了。

image-20220115211451964

之后调用转账方法:

这里有多个账号,我们可以直接切换。拿到2个账号

账号1:0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 合约创建账号

账号2:0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 需要转账的账号

image-20220115211520286

我们调用send()方法,向0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2账号转账100

image-20220115211836642

成功转账100个代币

调用查询方法,账号2正好有100个代币.至此就完成了发布,以及一系列转账等操作了。

image-20220115211935307

通过以上练习,大概能初步理解solidily了

以太坊虚拟机

上述这些都是跑在以太坊虚拟机上的,大家可以简单了解下以太坊虚拟机相关的知识:

以太坊虚拟机

注意的点:

Gas

一经创建,每笔交易都收取一定数量的 gas ,目的是限制执行交易所需要的工作量和为交易支付手续费。EVM 执行交易时,gas 将按特定规则逐渐耗尽。

gas price 是交易发送者设置的一个值,发送者账户需要预付的手续费= gas_price * gas 。如果交易执行后还有剩余, gas 会原路返还。

无论执行到什么位置,一旦 gas 被耗尽(比如降为负值),将会触发一个 out-of-gas 异常。当前调用帧(call frame)所做的所有状态修改都将被回滚。

所以在web3js调用的时候,也要注意gas费用,之前经常碰到gas太低而失败的例子。