node.js
npm
安装solidity编译器solc:npm install -g solc
上面的命令将安装solcjs程序,并使其在整个系统中都可用。
Remix
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版本。
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 按钮显示结果。
值类型

地址类型

引用类型

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

局部变量
变量值仅在函数执行过程中有效的变量,函数退出后,变量无效。函数参数是局部变量。
全局变量
全局工作区中存在的特殊变量,提供有关区块链和交易属性的信息。

局部变量的作用域仅限于定义它们的函数,但是状态变量可以有三种作用域类型。
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; // 访问状态变量
}
}
在合约中声明和使用的变量都有一个数据位置,指明变量值应该存储在哪里。合约变量的数据位置将会影响Gas消耗量。
Solidity 提供4种类型的数据位置。
Storage
Memory
Calldata
Stack
该存储位置 存储永久数据,这意味着该数据可以被合约中的所有函数访问。可以把它视为计算机的硬盘数据,所有数据都永久存储。
保存在存储区(Storage)中的变量,以智能合约的状态存储,并且在函数调用之间保持持久性。与其他数据位置相比,存储区数据位置的成本较高。
内存位置是临时数据,比存储位置便宜。它只能在函数中访问。
通常,内存数据用于保存临时变量,以便在函数执行期间进行计算。一旦函数执行完毕,它的内容就会被丢弃。你可以把它想象成每个单独函数的内存(RAM)。
Calldata是不可修改的非持久性数据位置,所有传递给函数的值,都存储在这里。此外,Calldata是外部函数的参数(而不是返回参数)的默认位置。
堆栈是由EVM (Ethereum虚拟机)维护的非持久性数据。EVM使用堆栈数据位置在执行期间加载变量。堆栈位置最多有1024个级别的限制。
可以看到,要永久性存储,可以保存在存储区(Storage)。




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






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


转义字符:

数组是一种数据结构,它是存储同类元素的有序集合。
数组中的特定元素由索引访问,索引值从0开始。例如,声明一个数组变量,如numbers,可以使用numbers[0]、numbers[1]和…,数字[99]表示单个变量。
数组可以是固定大小的,也可以是动态长度的。
对于存储(storage)数组,元素类型可以是任意的(可以是其他数组、映射或结构)。对于内存(memory)数组,元素类型不能是映射类型,如果它是一个公共函数的参数,那么元素类型必须是ABI类型。
类型为bytes和字符串的变量是特殊数组。bytes类似于byte[],但它在calldata中被紧密地打包。字符串等价于bytes,但(目前)不允许长度或索引访问。
因此,相比于byte[],bytes应该优先使用,因为更便宜。



length 获取动态数组长度
push
动态存储数组和bytes(不是字符串)有一个名为push的成员函数,可用于在数组末尾追加一个元素,函数返回新的长度。
枚举将一个变量的取值限制为几个预定义值中的一个。精确使用枚举类型有助于减少代码中的bug。
如下一个选择小、中、大的枚举:

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



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);
是全局可用的变量,提供关于区块链的信息。下面列出了常用的全局变量:

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


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

修饰符定义中出现特殊符号_的地方,用于插入函数体。如果在调用此函数时,满足了修饰符的条件,则执行该函数,否则将抛出异常。
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;
}
}
添加上view关键字的函数称为视图函数。其不会修改状态。若函数存在以下任一语句,则被视为修改状态,编译器将抛出警告。

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

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

同一个作用域内,相同函数名可以定义多个函数。这些函数的参数(参数类型或参数数量)必须不一样。仅仅是返回值不一样不被允许。
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);
}
}
Solidity 也提供了内置的数学函数。下面是常用的数学函数:
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);
}
}
Solidity 提供了常用的加密函数。以下是一些重要函数:
pragma solidity ^0.5.0;
contract Test {
function callKeccak256() public pure returns(bytes32 result){
return keccak256("ABC");
}
}
在智能合约中,直接向一个地址转账时,如该地址是一个合约地址,合约中可以编写代码,拒绝接受付款,导致交易失败。为避免这种情况,通常会使用提款模式。
提款模式是让收款方主动来提取款项,而不是直接转账给收款方。

示例:直接转账给收款方。
这是个比富游戏,智能合约接收用户发送的款项(以太),金额最高的将获得首富头衔,前一位首富失去头衔,但将获得金钱补偿,当前首富发送的款项,将转账给前首富(示例中此处使用直接转账)。
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);
}
}
对合约进行访问限制,是一种常见做法。
默认情况下合约是只读的,除非将合约状态指定为public。
使用限制访问修饰符,我们可以限制谁能修改合约状态,或者调用合约函数等操作。
下面示例中,创建了多个修饰符:
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;
}
}
Solidity中,合约类似于c++中的类。合约包含以下部分:
与Java、C++类的继承一样,Solidity中合约继承是扩展合约功能的一种方式。Solidity支持单继承和多继承。Solidity中,合约继承的重要特点:
构造函数是使用constructor关键字声明的特殊函数,用于初始化合约的状态变量。合约中构造函数是可选的,可以省略。
构造函数有以下重要特性:
类似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;
}
}
接口类似于抽象合约,使用interface关键字创建,接口只能包含抽象函数,不能包含函数实现。以下是接口的关键特性:
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;
}
}
库类似于合约,但主要作用是代码重用。库中包含了可以被合约调用的函数。
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;
}
}
事件是智能合约发出的信号。智能合约的前端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;
}
}
按照惯例,事件名称以大写字母开头,以区别于函数。
用JavaScript监听事件
下面的JavaScript代码侦听Increment事件,并更新UI。

索引(indexed参数)
一个事件最多有3个参数可以标记为索引。可以使用索引参数有效地过滤事件。下面的代码增强了前面的示例,来跟踪多个计数器,每个计数器由一个数字ID标识:


Solidity 提供了很多错误检查和错误处理的方法。通常,检查是为了防止未经授权的代码访问,当发生错误时,状态会恢复到初始状态。
assert(bool condition) − 如果不满足条件,此方法调用将导致一个无效的操作码,对状态所做的任何更改将被还原。这个方法是用来处理内部错误的。
require(bool condition) − 如果不满足条件,此方法调用将恢复到原始状态。此方法用于检查输入或外部组件的错误。
require(bool condition, string memory message) − 如果不满足条件,此方法调用将恢复到原始状态。此方法用于检查输入或外部组件的错误。它提供了一个提供自定义消息的选项。
revert() − 此方法将中止执行并将所做的更改还原为执行前状态。
revert(string memory reason) − 此方法将中止执行并将所做的更改还原为执行前状态。它提供了一个提供自定义消息的选项。