目录

  • 1. 本地环境
  • 2. 在线开发环境
  • 3. 基础语法
  • 4. 第一个程序
  • 5. 数据类型
  • 6. 变量
  • 7. 变量作用域
  • 8. 数据存放位置
    • 8.1 Storage
    • 8.2 Memory
    • 8.3 Calldata
    • 8.4 Stack
  • 变量的数据存放位置规则
    • 赋值的数据位置规则
  • 9. 字符串
  • 10. 数组
  • 11. 枚举
  • 12. 结构体
  • 13. 映射(mapping)类型
  • 14. 以太单位
  • 15. 特殊/全局变量
  • 16. 函数
    • 16.1 函数修饰符
    • 16.2 View(视图)函数
    • 16.3 Pure(纯)函数
    • 16.4 fallback(回退)函数
    • 16.5 函数重载
    • 16.6 数学函数
    • 16.7 加密函数
  • 17. 常用模式
    • 17.1 提款模式
    • 17.2 限制访问
  • 18. 智能合约
    • 18.1 可见性
    • 18.2 合约继承
    • 18.3 构造函数
    • 18.4 抽象合约
    • 18.5 接口
    • 18.6 库
    • 18.7 事件(Event)
    • 18.8 错误处理
  • 19. 获取ABI文件和Bin文件

1. 本地环境

node.js
npm
安装solidity编译器solc:npm install -g solc
上面的命令将安装solcjs程序,并使其在整个系统中都可用。

2. 在线开发环境

Remix

3. 基础语法

pragma solidity >=0.4.0 <0.6.0;contract SimpleStorage { uint storedData; function set(uint x) public {storedData = x; } function get() public view returns (uint) {return storedData; }}

pragma指令,表示源代码是为Solidity version 0.4.0及以上版本编写的,但不包括version 0.6.0及以上版本。
pragma指令只对自己的源文件起作用,如果把文件B导入到文件A,文件B的pragma将不会自动应用于文件A。

pragma solidity ^0.4.0;

源文件不能用低于0.4.0版本的编译器编译,也不能用0.5.0版本及以上版本的编译器编译。这里第二个条件是用^加上的,表示不超过0.5.0版本。

4. 第一个程序

pragma solidity ^0.5.0;contract SolidityTest { constructor() public{ } function getResult() public view returns(uint){uint a = 1;uint b = 2;uint result = a + b;return result; }}

在remix中:
在Compiler选项卡下,单击 Compile 按钮,开始编译
在Run选项卡下,单击 Deploy 按钮进行部署
在Run选项卡下,选择 SolidityTest at 0x… 下拉
单击 getResult 按钮显示结果。

5. 数据类型

  • 值类型

  • 地址类型

  • 引用类型

6. 变量

Solidity 支持三种类型的变量:

状态变量 – 变量值永久保存在合约存储空间中的变量。
局部变量 – 变量值仅在函数执行过程中有效的变量,函数退出后,变量无效。
全局变量 – 保存在全局命名空间,用于获取区块链相关信息的特殊变量。

  • 状态变量

  • 局部变量
    变量值仅在函数执行过程中有效的变量,函数退出后,变量无效。函数参数是局部变量。

  • 全局变量
    全局工作区中存在的特殊变量,提供有关区块链和交易属性的信息。

7. 变量作用域

局部变量的作用域仅限于定义它们的函数,但是状态变量可以有三种作用域类型。

Public – 公共状态变量可以在内部访问,也可以通过消息访问。对于公共状态变量,将生成一个自动getter函数。
Internal – 内部状态变量只能从当前合约或其派生合约内访问。
Private – 私有状态变量只能从当前合约内部访问,派生合约内不能访问。

pragma solidity ^0.5.0;contract C { uint public data = 30; uint internal iData= 10; function x() public returns (uint) {data = 3; // 内部访问return data; }}contract Caller { C c = new C(); function f() public view returns (uint) {return c.data(); // 外部访问 }}contract D is C { uint storedData; // 状态变量 function y() public returns (uint) {iData = 3; // 派生合约内部访问return iData; } function getResult() public view returns(uint){uint a = 1; // 局部变量uint b = 2;uint result = a + b;return storedData; // 访问状态变量 }}

8. 数据存放位置

在合约中声明和使用的变量都有一个数据位置,指明变量值应该存储在哪里。合约变量的数据位置将会影响Gas消耗量。

Solidity 提供4种类型的数据位置。

Storage
Memory
Calldata
Stack

8.1 Storage

该存储位置 存储永久数据,这意味着该数据可以被合约中的所有函数访问。可以把它视为计算机的硬盘数据,所有数据都永久存储。

保存在存储区(Storage)中的变量,以智能合约的状态存储,并且在函数调用之间保持持久性。与其他数据位置相比,存储区数据位置的成本较高。

8.2 Memory

内存位置是临时数据,比存储位置便宜。它只能在函数中访问

通常,内存数据用于保存临时变量,以便在函数执行期间进行计算。一旦函数执行完毕,它的内容就会被丢弃。你可以把它想象成每个单独函数的内存(RAM)。

8.3 Calldata

Calldata是不可修改的非持久性数据位置,所有传递给函数的值,都存储在这里。此外,Calldata是外部函数的参数(而不是返回参数)的默认位置。

8.4 Stack

堆栈是由EVM (Ethereum虚拟机)维护的非持久性数据。EVM使用堆栈数据位置在执行期间加载变量。堆栈位置最多有1024个级别的限制。

可以看到,要永久性存储,可以保存在存储区(Storage)。

变量的数据存放位置规则

赋值的数据位置规则

数据可以通过两种方式从一个变量复制到另一个变量。一种方法是复制整个数据(按值复制),另一种方法是引用复制。从一个位置复制数据到另一个位置有一定的默认规则。

9. 字符串

Solidity 中,字符串值使用双引号(“)和单引号(‘)包括,字符串类型用string表示。字符串是特殊的数组,是引用类型。


转义字符:

10. 数组

数组是一种数据结构,它是存储同类元素的有序集合。

数组中的特定元素由索引访问,索引值从0开始。例如,声明一个数组变量,如numbers,可以使用numbers[0]、numbers[1]和…,数字[99]表示单个变量。

数组可以是固定大小的,也可以是动态长度的。

对于存储(storage)数组,元素类型可以是任意的(可以是其他数组、映射或结构)。对于内存(memory)数组,元素类型不能是映射类型,如果它是一个公共函数的参数,那么元素类型必须是ABI类型。

类型为bytes和字符串的变量是特殊数组。bytes类似于byte[],但它在calldata中被紧密地打包。字符串等价于bytes,但(目前)不允许长度或索引访问。

因此,相比于byte[],bytes应该优先使用,因为更便宜。


length 获取动态数组长度
push
动态存储数组和bytes(不是字符串)有一个名为push的成员函数,可用于在数组末尾追加一个元素,函数返回新的长度。

11. 枚举

枚举将一个变量的取值限制为几个预定义值中的一个。精确使用枚举类型有助于减少代码中的bug。

如下一个选择小、中、大的枚举:

12. 结构体

用于表示复合型数据。结构体是引用类型。

13. 映射(mapping)类型

14. 以太单位

Solidity 中,以太币的单位可以使用wei、finney、szabo或ether表示。

assert(1 wei == 1);assert(1 szabo == 1e12);assert(1 finney == 1e15);assert(1 ether == 1e18);assert(2 ether == 2000 fenny);

时间单位:

assert(1 seconds == 1);assert(1 minutes == 60 seconds);assert(1 hours == 60 minutes);assert(1 day == 24 hours);assert(1 week == 7 days);

15. 特殊/全局变量

是全局可用的变量,提供关于区块链的信息。下面列出了常用的全局变量:

16. 函数

函数是一组可重用代码的包装,接受输入,返回输出。

16.1 函数修饰符

函数修饰符用于修改函数的行为。例如,向函数添加条件限制。


修饰符定义中出现特殊符号_的地方,用于插入函数体。如果在调用此函数时,满足了修饰符的条件,则执行该函数,否则将抛出异常。

pragma solidity ^0.5.0;contract Owner { address owner; constructor() public {owner = msg.sender; } // 定义修饰符 onlyOwner 不带参数 modifier onlyOwner {require(msg.sender == owner);_; } // 定义修饰符 costs 带参数 modifier costs(uint price) {if (msg.value >= price) { _;} }}contract Register is Owner { mapping (address => bool) registeredAddresses; uint price; constructor(uint initialPrice) public { price = initialPrice; } // 使用修饰符 costs function register() public payable costs(price) {registeredAddresses[msg.sender] = true; } // 使用修饰符 onlyOwner只有自己可以修改价格 function changePrice(uint _price) public onlyOwner {price = _price; }}

16.2 View(视图)函数

添加上view关键字的函数称为视图函数。其不会修改状态。若函数存在以下任一语句,则被视为修改状态,编译器将抛出警告。

  • 修改状态变量。
  • 触发事件。
  • 创建合约。
  • 发送以太。
  • 调用任何不是视图函数或纯函数的函数。
  • 使用selfdestruct。
  • 使用底层调用
  • 使用包含某些操作码的内联程序集。

16.3 Pure(纯)函数

添加上pure关键字的函数被称为纯函数。纯函数不读取或修改状态。如果函数中存在以下语句,则被视为读取状态,编译器将抛出警告。

  • 读取状态变量。
  • 访问 address(this).balance 或
    .balance
  • 访问任何区块、交易、msg等特殊变量(msg.sig 与 msg.data 允许读取)。
  • 调用任何不是纯函数的函数。
  • 使用包含特定操作码的内联程序集。

16.4 fallback(回退)函数

fallback(回退) 函数是合约中的特殊函数。它有以下特点:

  • 当合约中不存在的函数被调用时,将调用fallback函数。
  • 被标记为外部(external)函数。
  • 它没有名字。
  • 它没有参数。
  • 它不能返回任何东西。
  • 每个合约定义一个fallback函数。
  • 如果没有被标记为payable,则当合约收到无数据的以太币转账时,将抛出异常。

16.5 函数重载

同一个作用域内,相同函数名可以定义多个函数。这些函数的参数(参数类型或参数数量)必须不一样。仅仅是返回值不一样不被允许。

pragma solidity ^0.5.0;contract Test { function getSum(uint a, uint b) public pure returns(uint){return a + b; } function getSum(uint a, uint b, uint c) public pure returns(uint){return a + b + c; } function callSumWithTwoArguments() public pure returns(uint){return getSum(1,2); } function callSumWithThreeArguments() public pure returns(uint){return getSum(1,2,3); }}

16.6 数学函数

Solidity 也提供了内置的数学函数。下面是常用的数学函数:

  • addmod(uint x, uint y, uint k) returns (uint) 计算(x + y) % k,计算中,以任意精度执行加法,且不限于2^256大小。
  • mulmod(uint x, uint y, uint k) returns (uint) 计算(x * y) % k,计算中,以任意精度执行乘法,且不限于2^256大小。
pragma solidity ^0.5.0;contract Test {function callAddMod() public pure returns(uint){return addmod(4, 5, 3); } function callMulMod() public pure returns(uint){return mulmod(4, 5, 3); }}

16.7 加密函数

Solidity 提供了常用的加密函数。以下是一些重要函数:

  • keccak256(bytes memory) returns (bytes32) 计算输入的Keccak-256散列。
  • sha256(bytes memory) returns (bytes32) 计算输入的SHA-256散列。
  • ripemd160(bytes memory) returns (bytes20) 计算输入的RIPEMD-160散列。
  • ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address) 从椭圆曲线签名中恢复与公钥相关的地址,或在出错时返回零。函数参数对应于签名的ECDSA值: r – 签名的前32字节; s: 签名的第二个32字节; v: 签名的最后一个字节。这个方法返回一个地址。
pragma solidity ^0.5.0;contract Test {function callKeccak256() public pure returns(bytes32 result){return keccak256("ABC"); }}

17. 常用模式

17.1 提款模式

在智能合约中,直接向一个地址转账时,如该地址是一个合约地址,合约中可以编写代码,拒绝接受付款,导致交易失败。为避免这种情况,通常会使用提款模式。

提款模式是让收款方主动来提取款项,而不是直接转账给收款方。


示例:直接转账给收款方。

这是个比富游戏,智能合约接收用户发送的款项(以太),金额最高的将获得首富头衔,前一位首富失去头衔,但将获得金钱补偿,当前首富发送的款项,将转账给前首富(示例中此处使用直接转账)。

pragma solidity ^0.5.0;contract Test { address payable public richest; uint public mostSent; constructor() public payable {richest = msg.sender;mostSent = msg.value; } function becomeRichest() public payable returns (bool) {if (msg.value > mostSent) { // 转账给前首富,不安全方法,对方可以拒绝收款,导致交易失败,从而导致当前智能合约失败,游戏不能继续 richest.transfer(msg.value); richest = msg.sender; mostSent = msg.value; return true;} else { return false;} }}

示例

提款模式,让收款方(前首富)主动来提取款项,交易不会失败,游戏可以继续。

pragma solidity ^0.5.0;contract Test { address public richest; uint public mostSent; mapping (address => uint) pendingWithdrawals; constructor() public payable {richest = msg.sender;mostSent = msg.value; } function becomeRichest() public payable returns (bool) {if (msg.value > mostSent) { // 此处不直接转账,暂时记录应付款项 pendingWithdrawals[richest] += msg.value; richest = msg.sender; mostSent = msg.value; return true;} else { return false;} } // 收款方调用这个函数,主动提取款项 function withdraw() public {uint amount = pendingWithdrawals[msg.sender];pendingWithdrawals[msg.sender] = 0;msg.sender.transfer(amount); }}

17.2 限制访问

对合约进行访问限制,是一种常见做法。
默认情况下合约是只读的,除非将合约状态指定为public。
使用限制访问修饰符,我们可以限制谁能修改合约状态,或者调用合约函数等操作。

下面示例中,创建了多个修饰符:

  • onlyBy: 限制可以调用该函数的调用者(根据地址)。
  • onlyAfter: 限制该函数只能在特定的时间段之后调用。
  • costs: 调用方只能在提供特定值的情况下调用此函数。
pragma solidity ^0.5.0;contract Test { address public owner = msg.sender; uint public creationTime = now; modifier onlyBy(address _account) {require( msg.sender == _account, "Sender not authorized.");_; } function changeOwner(address _newOwner) public onlyBy(owner) {owner = _newOwner; } modifier onlyAfter(uint _time) {require( now >= _time, "Function called too early.");_; } function disown() public onlyBy(owner) onlyAfter(creationTime + 6 weeks) {delete owner; } modifier costs(uint _amount) {require( msg.value >= _amount, "Not enough Ether provided.");_;if (msg.value > _amount) msg.sender.transfer(msg.value - _amount); } function forceOwnerChange(address _newOwner) public payable costs(200 ether) {owner = _newOwner;if (uint(owner) & 0 == 1) return; }}

18. 智能合约

Solidity中,合约类似于c++中的类。合约包含以下部分:

  • 构造函数 – 使用constructor关键字声明的特殊函数,每个合约执行一次,在创建合约时调用。
  • 状态变量 – 用于存储合约状态的变量。
  • 函数 – 智能合约中的函数,可以修改状态变量来改变合约的状态。

18.1 可见性

  • external − 外部函数由其他合约调用,要在合约内部调用外部函数,使用this.function_name()的方式。状态变量不能标记为外部变量
  • public − 公共函数/变量可以在外部和内部直接使用。对于公共状态变量,Solidity为其自动创建一个getter函数。
  • internal − 内部函数/变量只能在内部或派生合约中使用。
  • private − 私有函数/变量只能在内部使用,派生合约中不能使用。

18.2 合约继承

关键字:is 。 A继承自B:A is B
与Java、C++类的继承一样,Solidity中合约继承是扩展合约功能的一种方式。Solidity支持单继承和多继承。Solidity中,合约继承的重要特点:

  • 派生合约可以访问父合约的所有非私有成员,包括内部函数和状态变量。但是不允许使用this。
  • 如果函数名保持不变,则允许函数重写。如果输出参数不同,编译将失败。
  • 可以使用super关键字或父合约名称调用父合约的函数。
  • 在多重继承的情况下,使用super的父合约函数调用,优先选择被最多继承的合约。

18.3 构造函数

构造函数是使用constructor关键字声明的特殊函数,用于初始化合约的状态变量。合约中构造函数是可选的,可以省略。

构造函数有以下重要特性:

  • 一个合约只能有一个构造函数。
  • 构造函数在创建合约时执行一次,用于初始化合约状态。
  • 在执行构造函数之后,合约最终代码被部署到区块链。合约最终代码包括公共函数和可通过公共函数访问的代码。构造函数代码或仅由构造函数使用的任何内部方法不包括在最终代码中。
  • 构造函数可以是公共的,也可以是内部的。
  • 内部构造函数将合约标记为抽象合约。
  • 如果没有定义构造函数,则使用默认构造函数。

18.4 抽象合约

类似java中的抽象类,抽象合约至少包含一个没有实现的函数(抽象函数)。通常,抽象合约作为父合约,被用来继承,在继承合约中实现抽象函数,抽象合约也可以包含有实现的函数

如果派生合约没有实现抽象函数,则该派生合约也将被标记为抽象合约。

pragma solidity ^0.5.0;contract Calculator { // 抽象合约  function getResult() public view returns(uint);}contract Test is Calculator { function getResult() public view returns(uint) {// 实现父合约抽象函数uint a = 1;uint b = 2;uint result = a + b;return result; }}

18.5 接口

接口类似于抽象合约,使用interface关键字创建,接口只能包含抽象函数,不能包含函数实现。以下是接口的关键特性:

  • 接口的函数只能是外部类型。
  • 接口不能有构造函数。
  • 接口不能有状态变量。
  • 接口可以包含enum、struct定义,可以使用interface_name.访问它们。
pragma solidity ^0.5.0;interface Calculator { function getResult() external view returns(uint);}contract Test is Calculator { constructor() public {} function getResult() external view returns(uint){uint a = 1; uint b = 2;uint result = a + b;return result; }}

18.6 库

库类似于合约,但主要作用是代码重用。库中包含了可以被合约调用的函数。

Solidity中,对库的使用有一定的限制。以下是库的主要特征。

  • 如果库函数不修改状态,则可以直接调用它们。这意味着纯函数或视图函数 只能从库外部调用。
  • 库不能被销毁,因为它被认为是无状态的。
  • 库不能有状态变量。
  • 库不能继承任何其他元素。
  • 库不能被继承。
pragma solidity ^0.5.0;library Search { //库的定义 function indexOf(uint[] storage self, uint value) public view returns (uint) {for (uint i = 0; i < self.length; i++) if (self[i] == value) return i;return uint(-1); }}contract Test { uint[] data; constructor() public {data.push(1);data.push(2);data.push(3);data.push(4);data.push(5); } function isValuePresent() external view returns(uint){uint value = 4;// 使用库的函数搜索数组中是否存在值uint index = Search.indexOf(data, value);return index; }}

18.7 事件(Event)

事件是智能合约发出的信号。智能合约的前端UI,例如,DApps、web.js,或者任何与Ethereum JSON-RPC API连接的东西,都可以侦听这些事件。事件可以被索引,以便以后可以搜索事件记录。

事件在区块链中的存储:
区块链是一个区块链表,这些块的内容基本上是交易记录。每个交易都有一个附加的交易日志,事件结果存放在交易日志里(交易收据)。合约发出的事件,可以使用合约地址访问。

Solidity中,要定义事件,可以使用event关键字(在用法上类似于function关键字)。然后可以在函数中使用emit关键字触发事件。

// 声明一个事件event Deposit(address indexed _from, bytes32 indexed _id, uint _value);// 触发事件emit Deposit(msg.sender, _id, msg.value);

示例:创建合约并发出一个事件:

pragma solidity ^0.5.0;contract Counter {uint256 public count = 0;event Increment(address who); // 声明事件function increment() public {emit Increment(msg.sender); // 触发事件count += 1;}}
  • event Increment(address who) 声明一个合约级事件,该事件接受一个address类型的参数,该参数是执行increment操作的账户地址。
  • emit Increment(msg.sender) 触发事件,事件会记入区块链中

按照惯例,事件名称以大写字母开头,以区别于函数

用JavaScript监听事件

下面的JavaScript代码侦听Increment事件,并更新UI。

索引(indexed参数)

一个事件最多有3个参数可以标记为索引。可以使用索引参数有效地过滤事件。比如前端可以根据索引进行搜索与过滤!下面的代码增强了前面的示例,来跟踪多个计数器,每个计数器由一个数字ID标识:


前端可以搜索过滤访问,所有which为1的 为2的…

18.8 错误处理

Solidity 提供了很多错误检查和错误处理的方法。通常,检查是为了防止未经授权的代码访问,当发生错误时,状态会恢复到初始状态。

  • assert(bool condition) − 如果不满足条件,就触发错误并中止合约的执行,此方法调用将导致一个无效的操作码,对状态所做的任何更改将被还原。这个方法是用来处理内部错误的(判断输出条件是否为真)。

  • require(bool condition) − 如果不满足条件,就触发错误并中止合约的执行,此方法调用将恢复到原始状态。(用于检查输入,例如函数的参数或者交易对象的字段)。

  • require(bool condition, string memory message) − 提供了一个提供自定义消息的选项。

  • revert() − 此方法将立即中止执行并将所做的更改还原为执行前状态。

  • revert(string memory reason) − 此方法将中止执行并将所做的更改还原为执行前状态。它提供了一个提供自定义消息的选项。

19. 获取ABI文件和Bin文件

1、确保本机安装了solc

npm install -g solc

2、新建一个.SOL文件
3、将写好的合约代码粘贴到文件中
4、在此目录位置cmd,打开控制台
5、使用如下命令编译,完成后就会在此目录位置生成ABI文件和bin文件。

solcjs --abi --bin Storage.sol