Solidity简介
Solidity是一门面向合约的高级编程语言。该语言设计的目的是能在 以太坊虚拟机(EVM)上运行。
Solidity是静态类型语言,支持继承、库 和 复杂的用户定义类型等特性。
目前尝试Solidity编程的最好方式是
Remix
。
Pragmas
关键字 pragma 版本标识指令,用来启用某些编译器检查。
版本 标识pragma 指令通常只对
本文件
有效,所以pragma要写到所有源文件里。
使用import导入其他文件, 其标识pragma不会从跟着被导入。
pragma solidity ^0.5.2;
不允许低于 0.5.2 也不允许高于(包含) 0.6.0 版本的编译器编译(第二个条件因使用 ^ 被添加)。
可以使用更复杂的规则来指定编译器的版本,表达式遵循
npm 版本语义
。
pragma solidity >=0.4.22 < 0.8.0;
导入其他源文件
Solidity 支持的导入语句来模块化代码,其语法跟 JavaScript(从 ES6 起)非常类似。
// 不推荐
import "filename";
// 推荐, 使用时直接 symbolName.***
import * as symbolName from "filename";
import {aaa as alias, bbb} from "filename";
注释
// 这是一个单行注释。
/*
这是一个
多行注释。
*/
合约结构
在 Solidity 语言中,把定义合约 的 contract 改成 class 就是 其他面向对象编程语言中的
类
。
每个合约中可以包含 状态变量、 函数、 函数 , 事件 Event, 错误(Errors), 结构体 和 枚举类型 的声明,且 合约可以从 其他合约 继承。
状态变量
状态变量是
永久存储
在 合约 中的 值。
pragma solidity >=0.4.0 <0.9.0;
contract TinyStorage {
uint storedXlbData; // 状态变量
// ...
}
函数
函数通常在合约内部定义,但也可以在合约外定义。
函数调用 可发生在合约内部或外部。
pragma solidity ^0.5.2;
contract Wtt {
// 定义函数
function Aaa() public payable {
// ...
}
}
function bbb(uint x) pure returns (uint) {
return x * 2;
}
事件
contract Wtt {
event AaaBbb(address bidder, uint amount); // 定义事件
function bid() public payable {
// ...
emit AaaBbb(msg.sender, msg.value); // 触发事件
}
}
错误
error NotEnoughFunds(uint requested, uint available); // 定义错误
contract Token {
function aaa (address to, uint amount) public {
if (balance < amount)
revert NotEnoughFunds(amount, balance); // 使用revert激发 错误
}
}
结构体
contract Wtt {
struct Who { // 结构体
uint age;
bool gender;
address delegate;
}
}
枚举
contract Upchain {
enum State { Zero, One, Two, Three } // 枚举
}
类型
Solidity 是一种静态类型语言,这意味着每个变量都需要在 编译时 指定变量的类型。
“undefined”或“null”值的概念在Solidity中不存在,但是新声明的变量总是有一个 默认值 ,具体的默认值跟类型相关。
值类型
这些类型的变量将始终按值来传递。 也就是说,当这些变量被用作函数参数或者用在赋值语句中时,总会进行值拷贝。
布尔(bool)
bool aaa = true;
/*
运算符:
! (逻辑非)
&& (逻辑与, “and” )
|| (逻辑或, “or” )
== (等于)
!= (不等于)
*/
整型(int、uint)
int / uint :分别表示有符号和无符号的不同位数的整型变量。
支持关键字 uint8 到 uint256 (无符号,从 8 位到 256 位),以 8 位为步长递增。
uint 和 int 分别是 uint256 和 int256 的 别名。
int aaa = 123;
范围检查
:
Solidity中的整数是有取值范围的。
例如 uint32 类型的取值范围是 0 到 2**(32-1)
0.8.0 开始,算术运算有两个计算模式:
- “wrapping”(截断)模式 或称 “unchecked”(不检查)模式
- ”checked” (检查)模式
默认情况下采用 “checked” 模式下,即都会进行溢出检查,如果结果落在取值范围之外,调用会通过 失败异常 回退。
也可以通过 unchecked { … } 切换到 “unchecked”模式
定长浮点型(fixed、ufixed)
在关键字 fixedMxN 中,M表示小数总的位数,N表示小数点后的位数。
M 必须能整除 8,即 8 到 256 中所有8的倍数。
N 则可以是从 0 到 80 之间的任意数。
fixed8x2 aaa = 300000.14
地址(address)
这个类型 放到后面再说
定长字节数组
关键字有: bytes1、bytes2、…、bytes32
枚举(enum)
从0开始递增。
enum State { Zero, One, Two, Three };
引用类型
引用类型可以通过
多个不同的名称
修改它的值,
而值类型的变量,每次都有独立的副本。
因此,必须比值类型更谨慎地处理引用类型。
如果使用 引用类型,则 必须明确指明 数据
存储位置
:
内存memory
:即数据在内存中,因此数据仅在其生命周期内(函数调用期间)有效。不能用于外部调用。
存储storage
: 状态变量保存的位置,只要合约存在就一直存储.
调用数据calldata
: 用来保存 函数参数 的特殊数据位置,是一个 只读位置,不可编辑修改。
更改
数据位置 或 类型转换
都会自动进行一份拷贝。
数据位置与赋值行为 有以下情况:
- 在 存储storage 和 内存memory 之间相互赋值,会创建一份独立的拷贝。
- 从 存储storage 到 存储storage 的赋值 只 创建引用
- 从 内存memory 到 内存memory 的赋值 只 创建引用
- 其他的 向 存储storage 的赋值,总是进行拷贝。
数组(Array)
数组可以在声明时指定长度(定长数组),也可以动态调整大小(动态数组)。
uint[5] aaa;
int[] bbb;
数组元素可以是任何类型,但是 对于 内存型 的 数组,其元素不能是 Mapping类型。
访问超出 数组长度 的 元素 会导致异常。
- 创建 内存数组
可使用
new 关键字
在 内存memory 中 创建 动态长度数组。
该动态数组 要区分于 存储storage 数组,不能通过.push改变 内存数组 的大小。
所以 必须提前计算好 所需的 大小 再创建内存数组。
uint[] memory x = new uint[](3);
x[0] = 1;
x[1] = 3;
x[2] = 4;
需要注意的是,定长的 内存memory 数组并不能赋值给变长的 内存memory 数组
- 数组成员
- length:
表示当前数组的长度。- push():
添加新的 零初始化 元素 到数组末尾,并返回元素引用- pop():
用来从数组末尾删除元素。
- 数组切片
数组切片是数组连续部分的视图,用法如:x[start:end]。
x[start:end] 的第一个元素是 x[start] , 最后一个元素是 x[end – 1] 。
如果 start 比 end 大或者 end 比数组长度还大,将会抛出异常。
start 和 end 都可以是可选的: start 默认是 0, 而 end 默认是数组长度。
数组切片没有 类型名称,这意味着没有变量可以将数组切片作为类型,它们仅存在于中间表达式中。
bytes 和 string 也是数组
bytes 和 string 类型的变量是特殊的数组,但 不允许 用 长度 或 索引 来访问。
结构体(Struct)
通过构造结构体的形式定义新的类型。
结构体 可以 作为 mapping 和 数组 的 元素成员,
但它并不能包含自身。 这个限制是有必要的,因为结构体的 大小 必须是 有限的。
映射(Mapping)
KeyType 可以是 任何基本类型,
ValueType 可以 任何类型。
映射是没有长度的,映射只能是 存储storage 的 数据位置,
mapping(address => uint) public balances;
- 可迭代映射
映射本身是无法遍历的,即无法枚举所有的键。
不过,可以在它们之上实现一个数据结构来进行迭代。
地址类型
地址类型有两种形式:
address
保存一个20字节的值(以太坊地址的大小)。
address payable
可支付地址,与 address 相同,不过有成员函数 transfer 和 send 。
这种区别背后的思想是 可以向 address payable 发送以太币,
而不能向一个普通的 address 发送以太币,
比如,address可能是一个智能合约地址,并且不支持接收以太币。
- 类型转换
允许从 address payable 到 address 的隐式转换,
而从 address 到 address payable 必须显示的转换, 通过
payable(<address>)
.
- 地址类型 成员变量
- .balance(uint256)
查询该地址的 ether余额,单位是Wei。
- .transfer(uint256 amount)
向指定 地址 发送 数量为 amount的 ether(单位wei),
失败时 抛出异常,发送2300gas的矿工费,不可调节。
- .send(uint256 amount) returns (bool)
send 是 transfer 的低级版本。向指定 地址 发送 数量为 amount的 ether(单位wei),
失败时 返回false,发送2300gas的矿工费,不可调节。
- .call(bytes memory) returns (bool,bytes memory)
发出底层函数CALL,失败是 返回 false,发送所有可用 gas,可以调节。
- .delegatecall(bytes memory) returns (bool,bytes memory)
发出底层函数delegatecall,失败是 返回 false,发送所有可用 gas,可以调节。
- .staticcall(bytes memory) returns (bool,bytes memory)
发出底层函数staticcall,失败是 返回 false,发送所有可用 gas,可以调节。
函数 call , delegatecall 和 staticcall 都是非常低级的函数,应该只把它们当作 最后一招 来使用,因为它们破坏了 Solidity 的类型安全性。
类型转换
隐式转换
在某些情况下,编译器会自动进行隐式类型转换, 这些情况包括: 在赋值, 参数传递给函数以及应用运算符时。
通常,如果可以进行值类型之间的隐式转换, 并且不会丢失任何信息。 都是可以隐式类型转换
uint8 y;
uint16 z;
uint32 x = y + z;
显式转换
如果某些情况下编译器不支持隐式转换,但是你很清楚你要做的结果,这种情况可以考虑显式转换。 注意这可能会发生一些无法预料的后果,因此一定要进行测试,确保结果是你想要的!
int8 y = -3;
uint x = uint(y);
如果一个类型显式转换成更小的类型,相应的高位将被舍弃
uint32 a = 0x12345678;
uint16 b = uint16(a); // 此时 b 的值是 0x5678
如果将整数显式转换为更大的类型,则将填充左侧(即在更高阶的位置)
uint16 a = 0x1234;
uint32 b = uint32(a); // b 为 0x00001234 now
assert(a == b);
数据位置
所有的 引用类型 都有一个 额外属性 即 “数据位置描述”, 用来说明 数据是保存在 内存memory中 还是 存储storage中。
在不同的上下文中,数据 有默认的 位置,但是也可以在 类型 后面 增加关键字
storage
、
memory
进行修改。
- 函数参数:默认是 memory
- 引用类型 局部变量:默认是 storage,有点 反人类
- 值类型 局部变量:默认是 stack 栈
- 状态变量:即合约属性,强制是 storage。
特别要求
public公开可见的函数 的 参数 一定是 memory类型。
如果参数要求必须用 storage类型,则函数 应该置为 private 或 internal,这是为了防止 随意调用占用资源。
举例:
function aaa(uint[] storage arr) public {
arr.push(123);
}
公开函数 public 的参数 一定不能是 storage空间 中的变量的话,否则 别人 随便 调用的话, 会造成 storage空间 的浪费。
所以 如果 参数一定要是 storage空间 中的变量的话,
函数 应该设定为 私有函数 如下:
function aaa(uint[] storage arr) internal {
arr.push(123);
}
控制结构
JavaScript 中的大部分控制结构在 Solidity 中都是可用的,除了
switch 和 goto
。
因此 Solidity 中有
if, else, while, do, for, break, continue, return, ? :
。
Solidity还支持
try/ catch
语句形式的异常处理,但仅用于 外部函数调用 和合约创建调用。
使用:ref:revert 语句 可以触发一个”错误”。
用于表示条件的
括号
不可以 被省略,单语句体 两边的 花括号 可以被省略。
注意
:与 C 和 JavaScript 不同, Solidity 中 非布尔类型数值 不能转换为 布尔类型,因此
if (1) { ... }
的写法在 Solidity 中 无效 。
pragma solidity >=0.4.22 < 0.8.0;
contract MyCounter {
uint256 counter;
constructor() public {
counter = 1;
}
function getCounter() public view returns (uint256) {
return counter;
}
function IterAdd(uint amount) public {
for (uint i=0; i<amount; i++) {
if (i % 2 == 0) {
counter +=i;
}
}
}
function LetZero() public {
counter = 0;
}
}
合约
Solidity 中的 合约 类似于 面向对象语言中的类。
合约中有用于数据持久化的 状态变量,和可以修改 状态变量 的 函数。
调用另一个合约实例的函数时,会执行一个 EVM 函数调用,这个操作会切换执行时的上下文,这样,前一个合约的状态变量就不能访问了。
创建合约
方式
- UI用户界面使创建合约
一些集成开发环境,例如
Remix
, 使得 创建合约 的过程更加顺畅。
- 通过编程创建合约
在 以太坊 上通过 编程创建合约 最好使用
JavaScript API web3.js
构造器
合约的 构造函数(构造器) 是一个用关键字
constructor
声明的 匿名函数。
创建合约时
,构造器会自动执行一次。 构造函数是可选的。
一个合约只允许有一个构造函数,这意味着不支持重载。
pragma solidity >=0.4.22 < 0.8.0;
contract MyCounter {
uint256 counter;
// 构造器
constructor () public {
counter = 1;
}
}
构造函数 执行完毕后,合约的 最终代码 将 部署到区块链上。
此代码包括:所有公共外部函数、所有可以通过函数调用访问的函数。
部署的代码不包括:构造函数代码、构造函数调用的内部函数。
可见性 和 getter函数
可见性 标识符 的 定义位置
- 状态变量
在 类型 后面- 函数
在 参数列表 和 返回关键字 中间
contract C {
uint public data;
function setData(uint a) internal { data = a; }
}
状态变量 的 可见性
- public
对于 public 状态变量 会
自动生成
一个 getter函数,以便其他的合约读取他们的值。
当被另一个合约中使用时,外部方式访问 (如: this.x) 会调用getter 函数,而内部方式访问 (如: x) 会直接从存储中获取值。
Setter函数则不会被生成,所以其他合约不能直接修改其值。
- internal
内部可见性 状态变量 只能在它们 所定义的合约 和 派生合约(继承子类)中访问。
它们不能被外部访问。 这是 状态变量的
默认可见性
。
- private
私有 状态变量 在 派生合约 中是不可见的。
函数(方法) 的 可见性
由于 Solidity 有两种函数调用:1.外部调用 (会产生一个 EVM); 2.内部调用。
这里有 4 种可见性:
- external
外部可见性函数
作为 合约接口 的一部分,意味着我们可以从
其他合约
和
交易
中调用。
一个外部函数 f 不能从 内部调用(即 f 不起作用,但 this.f() 可以)。
- public
public 函数
是 合约接口 的一部分,可以在
内部
或
通过消息
调用。
- internal
内部可见性函数
访问 可以在
当前合约
或
派生的合约
访问,
不可以外部访问。
由于它们没有通过合约的ABI向外部公开,它们可以接受内部可见性类型的参数:比如映射或存储引用。
- private
private 函数
仅在
当前定义它们的合约
中使用,并且不能被派生合约使用。
contract C {
uint private data;
// public
function publicTest() public returns(uint) {
return data;
}
// internal
function internalTest(uint a, uint b) internal returns (uint) {
return a+b;
}
// private
function f(uint a) privateTest returns(uint b) {
return a + 1;
}
}
contract D {
function readData() public {
C c = new C();
uint local = c.publicTest(); // 可以
local = c.internalTest(); // 不可以
local = c.privateTest(3, 5); // 不可以
}
}
contract E is C {
function g() public {
C c = new C();
uint local = c.publicTest(); // 可以
local = c.internalTest(); // 可以
local = c.privateTest(3, 5); // 不可以
}
}
Getter 函数
编译器 会为所有
public 状态变量
自动创建 getter 函数。
getter 函数具有外部可见性。
如果你有一个数组类型的 public 状态变量,
那么你只能通过生成的 getter 函数访问数组的单个元素。
这个机制以避免返回整个数组时的高成本gas。
contract Wtt {
uint[] public arr;
// 返回单个元素
/* 自动生成 的 Getter 函数 和 状态变量 同名
其 逻辑如下,也可以 手动指定生成的Getter 函数,使得完成更复杂的功能。
function arr(uint i) public view returns (uint) {
return arr[i];
}
*/
// 返回整个数组
function getAllArr() public view returns (uint[] memory) {
return arr;
}
}
Constant 和 Immutable 状态变量
状态变量 声明为
constant (常量)
或者
immutable (不可变量)
时,
合约一旦部署之后,变量 将不再修改。
对于 constant 常量, 他的值在
编译时
确定,而对于 immutable, 它的值在
部署时
确定。
常量 和 不可变量 的 gas成本 要低得多。
不是 所有类型 的 状态变量 都支持用 constant 或 immutable 来修饰,
当前 仅支持 字符串 (仅常量) 和 值类型.
contract C {
string constant aaa = "abc";
uint immutable bbb;
}
函数
可以在合约 内部 和 外部 定义函数。外部定义 是从 0.7.0 之后才开始支持的。
合约之外的函数(也称为“
自由函数
”)始终具有隐式的
internal 可见性
。
它们的代码包含在 所有调用它们合约中,类似于 内部库函数。
pragma solidity >=0.7.1 <0.8.13;
// 定义 自由函数
function sum(uint[] memory arr) pure returns (uint s) {
for (uint i = 0; i < arr.length; i++)
s += arr[i];
}
contract ArrayExample {
function f(uint[] memory arr) public {
// 调用 自由函数
uint s = sum(arr);
}
}
状态可变性
- View 视图函数
可以将函数声明为 view 类型,这种情况下要保证
不修改 状态
。
contract C {
function f(uint a, uint b) public view returns (uint) {
return a * (b + 42) + block.timestamp;
}
}
- Pure 纯函数
函数可以声明为 pure ,这种情况下要保证
不读取 也 不修改 状态变量
。
contract C {
function f(uint a, uint b) public pure returns (uint) {
return a * (b + 42);
}
}
特别的函数
- receive 接收以太函数一个合约最多有一个 receive 函数,
不需要 function 关键字,也没有 参数 和 返回值,
必须是 external 可见性和 payable 修饰.
在对 合约
没有任何附加数据
的调用(通常是
对合约转账
)会执行 receive 函数.
pragma solidity ^0.6.0;
// 这个合约会 保留 所有发送给它的以太币,但是没有办法取回。
contract Sink {
event Received(address, uint);
receive() external payable {
emit Received(msg.sender, msg.value);
}
}
- Fallback 回退函数
合约可以最多有一个回退函数。没有function关键字。 必须是external可见性.
在合约调用中,
没有其他函数 与 给定的函数标识符 匹配
则fallback会被调用;
在
没有 receive 函数
时,也
没有提供附加数据对合约调用
,则fallback会被执行。
fallback函数 始终会 接收数据,但为了 能同时接收以太 ,必须标记为 payable 。
// 发送 到这个 合约 的 所有消息 都会调用fallback函数(因为该合约没有其它函数)。
// 向这个 合约 发送 以太币 会导致异常,因为 fallback 函数没有 `payable` 修饰符
contract Test {
fallback() external { x = 1; }
uint x;
}
函数重载
合约 可以具有 多个 不同参数 的 同名函数,称为“重载”(overloading),
这也适用于继承函数。以下示例展示了合约 A 中的重载函数 f。
contract A {
function aaa(uint value) public pure returns (uint out) {
out = value;
}
function aaa(uint value, bool really) public pure returns (uint out) {
if (really)
out = value;
}
}
事件 Events
Solidity 的 事件 是EVM的
日志功能 之上的 抽象
。
是以 太坊虚拟机(EVM)日志 基础设施 提供的一个 便利接口。
应用程序 可以通过 以太坊客户端 的 RPC接口 订阅 和 监听 这些事件。
事件在合约中可被继承。
当他们被调用时,会使
参数
被存储到
交易的日志
中,
交易日志 是一种区块链中的 特殊数据结构。
这些 日志 与 地址 相关联,被并入 区块链 中,只要区块可以访问就一直存在。
日志 和 事件 在合约内 不可直接被访问(甚至是 创建日志 的 合约 也不能访问)。
来捋这个关系:区块链 是打包一系列 交易的区块组成的链条,
每一个 交易“收据” 会包含0到多个日志记录,
日志 中记录着 该智能合约 所触发的事件。
定义时间
使用
event
关键字来定义一个事件,如:
event EventName(address bidder, uint amount);
出发事件
触发事件可以在任何函数中调用,如:
function testEvent() public {
// 触发一个事件
emit EventName(msg.sender, msg.value);
}
监听事件
通过上面的介绍,可能大家还是不清楚事件有什么作用。
下面这个合约,在前端点击”Updata Info”按钮之后,虽然调用智能合约成功,但是当前的界面并没有得到更新。
使用事件监听,就可以很好的解决 数据更新的 问题,让看看如何实现。
pragma solidity ^0.4.21;
contract InfoContract {
string fName;
uint age;
function setInfo(string _fName, uint _age) public {
fName = _fName;
age = _age;
}
function getInfo() public constant returns (string, uint) {
return (fName, age);
}
}
首先,需要定义一个事件
然后,需要在setInfo函数中,触发Instructor事件,如:
pragma solidity ^0.4.21;
contract InfoContract {
// 这个事件中,会接受两个参数:name 和 age , 也就是 需要跟踪 的 两个信息。
event Instructor(
string name,
uint age
);
string fName;
uint age;
function setInfo(string _fName, uint _age) public {
fName = _fName;
age = _age;
// 出发时间
emit Instructor(_fName, _age);
}
function getInfo() public constant returns (string, uint) {
return (fName, age);
}
}
使用 JavaScript API 调用事件的用法如下:
<script>
if (typeof web3 !== 'undefined') {
web3 = new Web3(web3.currentProvider);
} else {
// set the provider you want from Web3.providers
web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:7545"));
}
web3.eth.defaultAccount = web3.eth.accounts[0];
var infoContract = web3.eth.contract(ABI INFO);
var info = infoContract.at('CONTRACT ADDRESS');
// 获取信息
info.getInfo(function(error, result){
if(!error)
{
$("#info").html(result[0]+' ('+result[1]+' years old)');
console.log(result);
}
else
console.error(error);
});
$("#button").click(function() {
info.setInfo($("#name").val(), $("#age").val());
});
</script>
现在可以不需要 info.getInfo()来获取信息,而改用监听事件获取信息,先定义一个变量引用事件,
然后使用.watch()方法来添加一个回调函数:
<script>
if (typeof web3 !== 'undefined') {
web3 = new Web3(web3.currentProvider);
} else {
// set the provider you want from Web3.providers
web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:7545"));
}
web3.eth.defaultAccount = web3.eth.accounts[0];
var infoContract = web3.eth.contract(ABI INFO);
var info = infoContract.at('CONTRACT ADDRESS');
// 改为:
var instructorEvent = info.Instructor();
instructorEvent.watch(function(error, result) {
if (!error)
{
$("#info").html(result.args.name + ' (' + result.args.age + ' years old)');
} else {
console.log(error);
}
});
$("#button").click(function() {
info.setInfo($("#name").val(), $("#age").val());
});
</script>
代码更新之后,可以在浏览器查看效果,这是点击”Updata Info”按钮之后,会及时更新界面.
错误 和 回退语句
错误(关键字error)提供了 一种方便 且 省gas 的方式
来向 用户 解释 一个操作 为什么会失败。
它们可以被定义在合约(包括接口和库)内部和外部。
错误必须与 revert 语句 一起使用。它会还原当前调用中的发生的所有变化,并将错误数据传回给调用者。
pragma solidity ^0.8.4;
/// 转账时,没有足够的余额。
/// @param available balance available.
/// @param required requested amount to transfer.
error InsufficientBalance(uint256 available, uint256 required);
contract TestToken {
function transfer(address to, uint256 amount) public {
if (amount > balance[msg.sender])
revert InsufficientBalance({
available: balance[msg.sender],
required: amount
});
}
}
错误不能被重写或覆盖,但是可以继承。
只要作用域不同,同一个错误可以在多个地方定义。
只能使用 revert 语句 创建 错误实例。
继承
Solidity 支持
多重继承
包括多态。
当一个合约从多个合约继承时,在区块链上只有 一个合约被创建 即 子合约,
所有基类合约(或称为父合约)的代码 都被编译到 子合约中。
总的来说,Solidity 的继承系统与 Python的继承系统 非常 相似,特别是多重继承方面, 但是也有一些 不同。
pragma solidity >=0.4.22 < 0.8.0;
contract aaa {
uint256 counter;
constructor () public {
counter = 1;
}
// 关键字`virtual`表示该函数可以在派生类中 被 重写。
function getCounter() virtual public view returns (uint256) {
return counter;
}
}
// 抽象类(抽象合约)
abstract contract bbb {
function testBbb(uint id) public virtual returns (uint id);
}
// 使用 is 继承合约 aaa
contract ccc is aaa, bbb {
function testBbb(uint id) public virtual returns (uint id){
return id;
}
}
接口
接口类似于抽象合约,但是它们不能实现任何函数。还有进一步的限制:
- 无法继承其他合约,不过可以继承其他接口。
- 接口中所有的函数都需要是 external,尽管在合约里可以是public
- 无法定义构造函数。
- 无法定义状态变量。
- 不可以声明修改器。
pragma solidity >=0.6.2 <0.8.0;
interface Token {
enum TokenType { Fungible, NonFungible }
struct Coin { string obverse; string reverse; }
function transfer(address recipient, uint amount) external;
}
特殊变量 和 函数
在 全局命名空间 中 已经存在了(预设了)一些 特殊的 变量 和 函数,
他们主要用来 提供关于 区块链 的 信息 或一些 通用的 工具函数。
区块和交易属性
- blockhash(uint blockNumber) returns (bytes32):指定区块的区块哈希 —— 仅可用于最新的 256 个区块且不包括当前区块,否则返回 0 。
- block.basefee (uint): 当前区块的基础费用,参考: (EIP-3198 和 EIP-1559)
- 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 ): 交易发起者(完全的调用链)
pragma solidity >=0.4.22 < 0.8.0;
contract MyCounter {
function consolelog() public view returns (address aaa,uint bbb, address ccc) {
aaa = msg.sender;
bbb = block.number;
ccc = tx.origin;
}
}
ABI
如果理解 API 就很容易了解 ABI。简单来说,API 是 程序 与 程序 间互动的接口。
这个接口 包含 程序提供 外界存取所需的 functions、variables 等。
ABI 也是 程序间 互动的接口,但程序是
被编译后
的 binary code。
所以同样的接口,但传递的是 binary 格式的信息。
所以 ABI 就要描述:
如何 decode/encode 程序间传递的 binary 信息
。
编译和部署
从 智能合约的代码 到 使用智能合约,大概包含
编写、编译、部署、调用
这几个步骤:
编写智能合约的代码(一般是用 Solidity 写)
编译智能合约的代码变成可在 EVM 上执行的 bytecode(binary code)。同时可以通过编译取得智能合约的 ABI
部署智能合约,实际上是把 bytecode 存储在链上(通过一个transaction),并取得一个专属于这个合约的地址
如果要写个程序调用这个智能合约,就要把信息发送到这个合约的地址(一样的也是通过一个 transaction)。Ethereum 节点会根据输入的信息,选择要执行合约中的哪一个 function 和要输入的参数
而要
如何知道這这个智能合约提供哪些 function 以及应该要传入什么样的参数呢?
这些信息就是
记录在智能合约的 ABI
!
Ethereum 智能合约 ABI
ABI 用一个 数组 表示,其中会包含 数个用 JSON 格式表示的 Function 或 Event。
Function 共有 7 个参数
- name:a string,function 名称
- type:a string,“function”, “constructor”, or “fallback”
- inputs:an array,function 输入的参数,包含:
- name:a string,参数名
- type:a string,参数的 data type(e.g. uint256)
- components:an array,如果输入的参数是 tuple(struct) type 才会有这个参数。描述 struct 中包含的参数类型
- outputs:an array,function 的返回值,和 inputs 使用相同表示方式。如果沒有返回值可忽略,值为 []
- payable:true,function 是否可收 Ether,预设为 false
- constant:true,function 是否会改写区块链状态,反之为 false
- stateMutability:a string,其值可能为以下其中之一:“pure”(不会读写区块链状态)、“view”(只读不写区块链状态)、“payable” and “nonpayable”(会改区块链状态,且如可收 Ether 为 “payable”,反之为 “nonpayable”)
Event 共有 4 个参数
- name: a string,event 的名称
- type: a string,always “event”
- inputs: an array,输入参数,包含:
- name: a string,参数名称
- type: a string,参数的 data type(e.g. uint256)
- components: an array,如果输入参数是 tuple(struct) type 才会有这个参数。描述 struct 中包含的信息类型
- indexed: true,如果这个参数被定义为 indexed ,反之为 false
- anonymous: true,如果 event 被定义为 anonymous
例子:
/*
这个智能合约包含:
data:一个可修改的 状态变量,会自动产生一个只能读取的 data() function
set():一个修改 data 值的 function
Set():一个在每次修写 data 时记录 Log 的 event
*/
pragma solidity ^0.4.20;
contract SimpleStorage {
uint public data;
event Set(address indexed _from, uint value);
function set(uint x) public {
data = x;
Set(msg.sender, x);
}
}
智能合约 ABI:
[
{
"constant": true,
"inputs": [],
"name": "data",
"outputs": [{"name": "","type": "uint256"}],
"payable": false,
"stateMutabㄒility": "view",
"type": "function"
},
{
"anonymous": false,
"inputs": [{"indexed": true,"name": "_from","type": "address"},{"indexed": false,"name": "value","type": "uint256"}],
"name": "Set",
"type": "event"
},
{
"constant": false,
"inputs": [{"name": "x","type": "uint256"}],
"name": "set",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
}
]
获取 abi
命令形式
npm install solc -g
solcjs aaa.sol --abi # 此时 在当前目录 就会 生成 相应的 abi文件。
# 也可以取得合约的 binary code:
solcjs aaa.sol --bin
GUI形式
同样的使用 Solidity Compiler,也可以用 Remix。
在合约 编译界面 的
Details
可以看到完整的 ABI。
也可以在 Settings 中指定 Compiler 版本。
项目实例
众筹项目
pragma solidity ^0.5.8;
contract zhongchouTest {
// 捐赠者
struct funder {
address funderAddress; // 捐赠者地址
uint toMoney; // 捐赠money
}
// 受益人对象
struct needer {
address payable neederAddress; // 受益人地址
uint goal; // 众凑总数(目标)
uint amount; // 当前募资金额
uint isFinish; // 募资是否完成
uint funderAccount; // 当前捐赠者人数
mapping(uint => funder) funderMap; // 映射,将捐赠者的id与捐赠者绑定在一起,从而得知是谁给受益人捐钱
}
uint Id; // 众筹项目id,由于 多个人需要帮助 故会发起多个 众筹项目,因此用 id进行 项目标识
mapping (uint => needer) neederMap; // 通过mapping将受益人id与收益金额绑定在一起,从而可以更好的管理受益人
// 新建众筹项目
/*
* _neederAddress: 受益人地址(项目发起者)
* _goal: 众筹目标
*/
function NewNeeder(address payable _neederAddress, uint _goal) public {
Id++; // 众筹项目id 从 1 开始
// 记录 项目id 和 受益人 的联系
neederMap[Id] = needer(address(_neederAddress), _goal, 0, 0, 0);
}
// 捐赠者给指定众筹id 打钱
/*
*_address: 捐赠者地址
*_id: 众筹项目id
*/
function contribue(address _address, uint _id) public payable {
require(msg.value > 0);
needer storage _needer = neederMap[_id]; // 通过 众筹项目id 获取 指定的 受益人对象
require(_needer.isFinish == 0); // 募资是否完成, 若完成则取消当前捐款
_needer.amount += msg.value; // 统计 到合约账户的 捐赠金额
_needer.funderAccount++; // 捐赠者个数
_needer.funderMap[_needer.funderAccount] = funder(_address, msg.value); // 记录 捐赠者 及 捐赠金额
}
// 捐赠是否完成,若完整,给 受益人 转账
/*
*_id: 众筹项目id
*/
function Iscompelete(uint _id) public payable {
needer storage _needer = neederMap[_id]; // 获取众筹项目
require(_needer.amount >= _needer.goal);
// transfer 向指定 地址 发送 数量为 amount的 ether(单位wei)
_needer.neederAddress.transfer(_needer.amount);// 合约账户 ===转账到===> 个人账户
_needer.isFinish = 1; // 若完成募资,则取消继续募资
}
// 募资完成时,退款给捐赠人
function returnBack(uint _id) public payable {
needer storage _needer = neederMap[_id]; // 获取众筹项目
require(_needer.funderMap[_needer.funderAccount].funderAddress == msg.sender);
uint returnMoney = _needer.funderMap[_needer.funderAccount].toMoney;
uint balance = address(this).balance;
balance -= returnMoney;
// msg.sender 合约调用者 地址
// transfer 向指定 地址 发送 数量为 amount的 ether(单位wei)
msg.sender.transfer(returnMoney);
}
// 查询合约余额
function getBalance() public view returns(uint,uint) {
// balance 查询该地址的 ether余额
return (address(this).balance, Id);
}
// 查看募资状态, 以 具名返回值 的形式 更方便查看 数据
function showData(uint _id) public view returns(uint goalNum, uint hasGetNum, uint funderNum, uint isFinish) {
goalNum = neederMap[_id].goal;
hasGetNum = neederMap[_id].amount;
funderNum = neederMap[_id].funderAccount;
isFinish = neederMap[_id].isFinish;
}
}