一. 智能合约概述
前言
区块链1.0: 2009年比特币的诞生。
区块链2.0: 以太坊的诞生,结合了区块链与智能合约技术的平台。
什么是智能合约
1996年,Nick Szabo在文章《Smart Contracts: Building Blocks For Digital Markets》中提出了智能合约的概念。
所谓“合约”,就是条文、合同一类的东西,里面记录了发生的条件与对应执行的条款,以支持确权等操作;所谓"智能",就意味着自动化、可编程。
所以,智能合约就是可编程的合同,也可以理解为一段自动执行的条文合同,在计算机中,就是一段自动执行的程序片段。它更易于合约保存,并且由确定的算法运行,给定输入,就得到对应的输出,极大保障了合约的执行力。
智能合约的现状与前景
从编程角度而言,智能合约就是一段代码。相比常规代码,智能合约具有许多差别与限制,例如:
- 单线程执行
- 代码执行会消耗资源,不能超出资源限制
- 目前难以获取链外数据,例如取得天气信息、比赛结果等
- 其他限制,如TPS
这些特点使得目前智能合约生态以链上资源的治理为核心。就像以太坊上各式各样的ERC标准与治理方案;EOS上有各种资源模型,比如CPU、RAM、Rex、Bancor协议等。
显然,就目前的生态而言,智能合约对现实世界的影响力有限。
但事物总是在发展的。目前,已有许多致力于突破这些限制的研究,典型的有Oracle(谕言机,但常被称为预言机),它允许智能合约和链外进行交互,这样就能大大提高智能合约的使用场景,彷佛一台电脑通上了网;再比如那些突破链自身性能瓶颈的尝试,例如支付通道、跨链、plasma、rollup,它们都从不同角度打破安全与性能的枷锁。
毋庸置疑,智能合约将扮演着越来越重要的角色,将来随着以太坊2.0的落地,也许会开启新一个区块链时代。
智能合约技术
以太坊采用了Solidity作为智能合约语言,Solidity 是一门为实现智能合约而创建的高级编程语言,能在允许以太坊程序的节点上运行。该语言吸收了C++、JavaScript的一些特性,例如它是静态类型语言,支持继承、库等。
二. solidity开发讲解
简单的示例:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract SimpleStorage {
uint storedData;
function set(uint x) public {
storedData = x;
}
function get() public view returns (uint) {
return storedData;
}
}
1. 源文件结构
源文件中可以包含任意多个 合约定义 、导入源文件指令 、 版本标识 指令、 结构体 、 枚举 和 函数 定义.
SPDX许可标识
SPDX:The Software Package Data Exchange
- 常见开源:
// SPDX-License-Identifier: MIT
- 私有或者无授权:
// SPDX-License-Identifier: UNLICENSED
版本标识
pragma solidity ^0.8.4;
或者 pragma solidity >=0.4.16 <0.9.0;
ABI Coder Pragma
Solidity 0.7.4 之前:pragma experimental ABIEncoderV2
Solidity 0.7.4 之后:pragma abicoder v2
导入文件
import "filename";
示例:
import "./helper.sol";
注释
// This is a single-line comment.
/*
This is a
multi-line comment.
*/
2. 合约结构
状态变量
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract SimpleStorage {
uint storedData; // State variable
// ...
}
函数
/ SPDX-License-Identifier: GPL-3.0
pragma solidity >0.7.0 <0.9.0;
contract TinyAuction {
function Mybid() public payable { // 定义函数
// ...
}
}
// Helper function defined outside of a contract
function helper(uint x) pure returns (uint) {
return x * 2;
}
函数修饰器
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
contract Purchase {
address public seller;
modifier onlySeller() { // Modifier
require(
msg.sender == seller,
"Only seller can call this."
);
_;
}
function abort() public view onlySeller { // Modifier usage
// ...
}
}
事件
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.21 <0.9.0;
contract SimpleAuction {
event HighestBidIncreased(address bidder, uint amount); // Event
function bid() public payable {
// ...
emit HighestBidIncreased(msg.sender, msg.value); // Triggering event
}
}
异常处理
使用revert
或者require
(推荐)
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
/// Not enough funds for transfer. Requested `requested`,
/// but only `available` available.
error NotEnoughFunds(uint requested, uint available);
contract Token {
mapping(address => uint) balances;
function transfer(address to, uint amount) public {
uint balance = balances[msg.sender];
if (balance < amount)
revert NotEnoughFunds(amount, balance);
balances[msg.sender] -= amount;
balances[to] += amount;
// ...
}
function transfer2(address to, uint amount) public {
uint balance = balances[msg.sender];
require(balance > amount," balance must be greater than amount");
balances[msg.sender] -= amount;
balances[to] += amount;
// ...
}
}
结构
pragma solidity >=0.4.0 <0.9.0;
contract Ballot {
struct Voter { // 结构体
uint weight;
bool voted;
address delegate;
uint vote;
}
}
枚举
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.25 <0.9.0;
contract Purchase {
enum State { Created, Locked } // Enum
}
3. 常用信息
数据类型
值类型:Booleans(true&false)、Integers(int8 to int256,uint8 to uint256)、Address(长度40位,20字节)、byte(byte1 to byte32)。
引用类型:struct、bytes(byte[])、string、mapping。
引用类型可以通过多个不同的名称修改它的值,而值类型的变量,每次都有独立的副本。因此,必须比值类型更谨慎地处理引用类型。 目前,引用类型包括结构,数组和映射
,如果使用引用类型,则必须明确指明数据存储哪种类型的位置(空间)里:
a. memory :数据在内存中,因此数据仅在其生命周期内(函数调用期间)有效。不能用于外部调用。
b. storage :状态变量保存的位置,只要合约存在就一直存储.
c. calldata:调用数据时用来保存函数参数的特殊数据位置,是一个只读位置,无需copy变量值到其他存储位置。类型修饰符:私有(private)、内部(internal)、公共(public)、外部(external);函数默认为internal,状态变量默认为private。另外函数还包含view(读取账本)和pure(不读取也不更改账本)。
详解:
private:仅内部访问。
internal: 仅内部以及派生类访问。
external:仅外部访问。
public:内部和外部都可以访问。
特殊变量和函数
-
abi.encode(...) returns (bytes)
: ABI - 对给定参数进行编码 -
abi.encodePacked(...) returns (bytes)
:对给定参数执行 紧打包编码 -
abi.encodeWithSelector(bytes4 selector, ...) returns (bytes)
: ABI- 对给定参数进行编码,并以给定的函数选择器作为起始的 4 字节数据一起返回 -
abi.encodeWithSignature(string signature, ...) returns (bytes)
:等价于abi.encodeWithSelector(bytes4(keccak256(signature), ...)
- block.blockhash(uint blockNumber) returns (bytes32):指定区块的区块哈希——仅可用于最新的 256 个区块且不包括当前区块
- block.chainid (uint): 当前链 id
- block.coinbase ( address ): 挖出当前区块的矿工地址
- block.difficulty ( uint ): 当前区块难度
- block.gaslimit ( uint ): 当前区块 gas 限额
- block.number ( uint ): 当前区块号
- block.timestamp ( uint): 自 unix epoch 起始当前区块以秒计的时间戳
- gasleft() returns (uint256) :剩余的 gas
- msg.data ( bytes ): 完整的 calldata
- msg.sender ( address ): 消息发送者(当前调用)
- msg.sig ( bytes4 ): calldata 的前 4 字节(也就是函数标识符)
- msg.value ( uint ): 随消息发送的 wei 的数量
- tx.gasprice (uint): 交易的 gas 价格
- tx.origin (address payable): 交易发起者(完全的调用链)
- address(this):当前合约的地址
命名规范:
智能合约命名并没有一个标准,不过团队内部可以按照一个行业共识的规范执行。经过实战,推荐以下风格(不强制),如下代码块。
- 合约命名:采用驼峰命名、首字母大写、且能表达对应的业务含义;
- 方法命名:采用驼峰命名、首字母小写、且能表达对应的业务含义;
- 事件命名:采用驼峰命名、首字母小写、且能表达对应的业务含义,以Event结尾;
- 合约变量:采用驼峰命名、以_开头,首字母小写、且能表达对应的业务含义;
- 方法入参:采用驼峰命名、首字母小写、且能表达对应的业务含义;
- 方法出参:建议只写出参类型,无需命名,特殊情况例外;
- 事件参数:同方法入参;
- 局部变量:同方法入参。
4. 示例
myContract.sol
和 helper.sol
。
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.4;
// pragma experimental ABIEncoderV2;
import "./helper.sol";
/**
* @title Storage
* @dev Store & retrieve value in a variable
*/
contract Storage {
address public seller;
constructor(){
seller = msg.sender;
}
modifier onlySeller() { // Modifier
require(
msg.sender == seller,
"Only seller can call this."
);
_;
}
uint256 number;
event storeEvent(string name,uint256 value);
/**
* @dev Store value in variable
* @param num value to store
*/
function store(uint256 num) public {
number = num;
emit storeEvent("number",number);
}
/**
* @dev Return value
* @return value of 'number'
*/
function retrieve() public view returns (uint256){
return number;
}
// only call by seller
function storeOnlySeller(uint256 num) public onlySeller {
// number = num;
number = Helper.add(num,1);
emit storeEvent("number",number);
}
// destory the contract
function destory() public onlySeller {
selfdestruct(payable(msg.sender));
}
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.4;
library Helper {
function add(uint256 a,uint256 b) public pure returns(uint256) {
return (a+b);
}
}
4. 编译器
-
solc :
a. solcjs:
注意: solc-js 项目是利用 Emscripten 从 C++ 版的 solc 跨平台编译为 JavaScript 的,因此,可在 JavaScript 项目中使用 solcjs(如同 Remix)。 具体介绍请参考 solc-js 代码库。
npm install -g solc
solcjs --bin --base-path . /myContract.sol
b. Docker
stable 仓库里的是已发布的版本,nightly 仓库则是在开发分支中的带有不稳定变更的版本。``` docker run ethereum/solc:stable --help 或者 docker run ethereum/solc:0.8.6 --help docker run -v /local/path:/sources ethereum/solc:stable -o /sources/output --abi --bin /sources/Contract.sol ```
c. 源码:Linux Packages、macOS Packages
5. 常见问题
EVM栈溢出
ComplierError: Stack too deep ,try removing local variables.
参数个数限制:
版本 | 入参个数 | 出参个数 | 参数总数 |
---|---|---|---|
0.4.25 | 16 | 13 | 16 |
0.8.4(>=0.7.0 <0.9.0) | 11 | 12 | 16 |
解决方案: 当参数太多时,可以使用struct进行参数封装。
示例:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.4.25;
contract SimpleStorage {
//ComplierError: Stack too deep ,try removing local variables.
// ^0.4.25: max size of in :16
function testInParamterSize(
string memory name,
bytes32 addr,
uint8 sex,
uint age,
bool isRich,
string memory name2,
bytes32 addr2,
uint8 sex2,
uint age2,
bool isRich2,
string memory name3,
bytes32 addr3,
uint8 sex3,
uint age3,
bool isRich3,
string memory name4
)
public
{
//TODO
}
//^0.4.25: max size of out :13
function testOutParamterSize(
)
public
returns(bool,bool,bool,string memory,uint8,
bool,bool,bool,string memory,uint8,
bool,bool,bool)
{
//TODO
}
//^0.4.25: in 3,out 13 total:16
function testTotalParamerSize(
string memory name,
bytes32 addr,
uint8 sex
)
public
returns(bool,bool,bool,string memory,uint8,
bool,bool,bool,string memory,uint8,
bool,bool,bool)
{
//TODO
}
//^0.4.25: in 15,out 1 total:16
function testTotalParamerSize1(
string memory name,
bytes32 addr,
uint8 sex,
uint age,
bool isRich,
string memory name2,
bytes32 addr2,
uint8 sex2,
uint age2,
bool isRich2,
string memory name3,
bytes32 addr3,
uint8 sex3,
uint age3,
bool isRich3
)
public
returns(bool)
{
//TODO
}
}
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract SimpleStorage {
//ComplierError: Stack too deep ,try removing local variables.
// max size of in :11
function testInParamterSize(
string memory name,
bytes32 addr,
uint8 sex,
uint age,
bool isRich,
string memory name2,
bytes32 addr2,
uint8 sex2,
uint age2,
bool isRich2,
string memory name3
)
public
{
//TODO
}
//^0.8.4: max size of out :12
function testOutParamterSize(
)
public
returns(bool,bool,bool,string memory,uint8,
bool,bool,bool,string memory,uint8,
bool,bool)
{
//TODO
}
//^0.8.4: in 4,out 12 total:16
function testTotalParamerSize(
string memory name,
bytes32 addr,
uint8 sex,
uint age2
)
public
returns(bool,bool,bool,string memory,uint8,
bool,bool,bool,string memory,uint8,
bool,bool)
{
//TODO
}
//^0.8.4: in 11,out 5 total:16
function testTotalParamerSize(
string memory name,
bytes32 addr,
uint8 sex,
uint age,
bool isRich,
string memory name2,
bytes32 addr2,
uint8 sex2,
uint age2,
bool isRich2,
string memory name3
)
public
returns(bool,bool,bool,string memory,uint8)
{
//TODO
}
}
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract SimpleStorage {
//^0.8.4: in 11,out 5 total:16
function testTotalParamerSize(
string memory name,
bytes32 addr,
uint8 sex,
uint age,
bool isRich,
string memory name2,
bytes32 addr2,
uint8 sex2,
uint age2,
bool isRich2,
string memory name3
)
public
returns(bool,bool,bool,string memory,zxl memory)
{
//TODO
}
struct zxl{
string name;
bytes32 addr;
uint8 sex;
uint age;
bool isRich;
string name2;
bytes32 addr2;
uint8 sex2;
uint age2;
bool isRich2;
string name3;
bytes32 addr3;
uint8 sex3;
uint age3;
bool isRich3;
string name4;
bytes32 addr4;
uint8 sex4;
uint age4;
bool isRich4;
}
}
无法正常debug
编写Solidity代码无法像其他高级语言一样进行debug。
解决方案:使用事件的log进行打印和展示更多内部变量。
示例如下
合约文件:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract HelloWorld {
string public name;
event Log(string oldValue, string newValue, address sender);
constructor() public{
name = "Hello, World!";
}
function set(string memory newValue) public{
emit Log(name, newValue, msg.sender);
name = newValue;
}
function get() public view returns(string memory){
return name;
}
}
日志信息:
logs [ { "from": "0xf8e81D47203A594245E36C48e151709F0C19fBe8", "topic": "0xf35471203c6fa92fdd58f4bc31d50b93ecf6f4e792a9fb3ae964ca758a3a4aa6", "event": "Log", "args": { "0": "Hello, World!", "1": "zxl", "2": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4", "oldValue": "Hello, World!", "newValue": "zxl", "sender": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4" } } ]