1

版本声明
首先,所有的 solidity 源码前面必须标明编译器版本
pragma solidity ^0.4.18; 这个就声明了版本是 0.4.18 到 0.5.0 的编译器中是可以工作的

合约
写一个合约的基本框架是:
contract name{
}

变量
下面来看一下怎么声明变量,状态变量会永久的保存在合约里
uint 表示无符号整数,int 表示有符号的
在 solidity 里面 uint 默认表示 uint256,其他的还有 uint8、uint16、uint32…
声明一个变量:uint id = 115;

结构体
solidity 也可以用结构体:下面声明了一个叫 Student 的结构体,有两个属性一个是 string 类型的 name,另一个是 uint 类型的 id
struct Student{
string name;
uint id;
}

数组
solidity 支持两种类型的数组,静态数组、动态数组
uint 类型的固定长度为 10 的静态数组 uint[10] id;
uint 类型的长度不定的动态数组 uint[] id;
也可以建立一个结构体类型的数组 Student[] students;

函数
习惯上,函数里面的变量都是以 _ 开头的
function sayhello (string _name, uint _id) {
}

使用结构体和数组
下面我们来了解一下怎么使用结构体和数组
Student yichen=Student(‘yichen’, 115); //创建一个新的 student
students.push(yichen); //把创建的 yichen 添加到 students 结构体数组里面
当然也可以一步完成
students.push(Student(‘yichen’, 115));

函数的属性
solidity 默认的属性是公共的(public),也就是说谁都可以调用,这样明显是不安全的,所以我们将它设置为私有的(private),只需要在函数后面加上一个 private 就可以,另外私有函数习惯名称前带个下划线
function _sayhello (string _name, uint _id) private {
}

函数的更多属性
想让函数返回一个值的话可以
string greeting = “hello yichen!”;
function _sayhello () returns (string){
return greeting;
}
在以太坊中,去执行一个函数是需要花费一些钱的(专业术语:gas),所以可以设置函数修饰符
我们可以发现上面那个函数并没有修改任何东西,所以可以给他设置一个 view 修饰符,表示它只是读取数据,没有改变或者写任何东西,那么运行这个函数的时候只需要去查询你本地保存的数据就可以,不需要全世界都知道并且把它写进区块中,所以不会消耗 gas
还有个 pure 修饰符,表示这个函数甚至不会访问合约里的任何东西,他的返回值完全取决于我们的输入
例如:
function _multiply(uint a, uint b) private pure returns (uint) {
return a * b;
}//这个函数接收我们的输入,然后把两个数的乘积返回给我们

Keccak256
Ethereum 内部有一个散列函数 keccak256,他会把一个字符串转换成 16 进制的数字

类型转换
uint8 -> uint16
小单位变到大单位,值不变
uint16 -> uint8
大单位变到小单位,变为 原值 mod 256
bytes8 -> bytes16
后面补 0
bytes16 -> bytes8
只取前面的 8 数
address -> uint
按照 uint 的单位从地址后面开始截取对应长度
address -> bytes
按照 bytes 的单位从地址前面开始截取对应长度
uint/bytes -> address
前面填充 0,直到符合 address 长度

事件

事件是以太坊虚拟机(EVM)日志功能中提供的一组方便的接口。当事件被触发时,它们会将参数保存到交易日志中——区块链中一种特殊的数据结构。这些日志与合约地址相关联,并且会被打包进区块中,因此可以被永久访问(不过 Serenity 版本或许会有所改变)。注意,日志和事件的数据是不能被合约访问的,即便是创建它们的合约也不行(不然常规的数据存储就没意义了)

event IntegersAdded(uint x, uint y, uint result);

function add(uint _x, uint _y) public {
uint result = _x + _y;
//触发事件,通知 app
IntegersAdded(_x, _y, result);
return result;
}

可以在前端监听这个事件,当触发的时候就做些事
YourContract.IntegersAdded(function(error, result) {
}

2

地址
以太坊的每个账户都有一个地址,这个地址就是这个账户的标识

映射
映射是以太坊中另一种存储和组织数据的方法,映射本质上是存储和查找数据所用的键值对我们看一个例子:
mapping (uint => string) userIdToName;
//这个例子中我们可以通过 id 去存储或查找用户名

msg.sender
msg.sender 是 solidity 中的一个全局变量,他表示的是当前调用者(或智能合约)的地址

1
2
3
4
5
6
7
8
9
10
11
12
mapping (address => uint) favoriteNumber;
//设置一个映射,键是address,值是uint
function setMyNumber(uint _myNumber) public {
// 写个函数来设置favoriteNumber
favoriteNumber[msg.sender] = _myNumber;
// 设置favoriteNumber映射,将_myNumber存储在msg.sender名下
}
function whatIsMyNumber() public view returns (uint) {
// 写一个查询favoriteNumber的函数
return favoriteNumber[msg.sender];
// 若调用者还没调用setMyNumber,则值为0
}

require
可以通过 require 来做一些限制,比如 x 要大于 10 才能继续执行,否则报错:
function test(string _name) returns (string){
retuire(keccak256(_name) == keccak256(“yichen”));
//solidity 不支持字符串直接比较,所以这里用散列值比较
return “hello”;
}
如果调用的时候是 test(“yichen”); 就会 返回 hello,否则报错

继承(Inheritance)
当代码很长的时候我们可以把它们拆分成不同的合约,通过继承来获得其他合约的功能

1
2
3
4
5
6
7
8
9
10
contract welcome {
function hi() public returns (string) {
return "hi!";
}
}
contract welcome2 is welcome {
function hello() public returns (string) {
return "hello";
}
}

引入(Import)
solidity 也支持 import(solidity 文件后缀是 sol)
import “./sayhello.sol”;

Storage 与 Memory
solidity 中有两个可以存储变量的地方 Storage 与 Memory
Storage 变量是永久的存储在区块链中的变量
Memory 变量则是临时的,当外部函数对某合约调用完成时,内存型变量即被移除
一个比较好的比喻是:Storage 理解为硬盘上存储的数据,Memory 理解为内存上的数据
状态变量(在函数之外声明的变量)默认为 Storage 形式,并永久写入区块链
在函数内部声明的变量是 Memory 型的,它们函数调用结束后消失

还有一些特殊情况,需要手动指定存储类型,比如:处理函数内部的结构体和数组

更多函数可见性
函数声明 private 之后,哪怕是继承这个合约的子合约都不能调用,所以可以使用更好的方法 internal
用 internal 声明的函数,继承了这个函数所在合约的子合约可以调用这个函数,同时又限制了其他人随便调用

external 声明的函数只能在合约之外调用,不能被合约内的其他函数调用

接口
如果我们想要与其他合约进行交互的话,需要声明一个接口
比如上面的那个最爱的数字的合约

1
2
3
4
5
6
7
8
9
contract LuckyNumber {
mapping (address => uint) favoriteNumber;
function setMyNumber(uint _myNumber) public {
favoriteNumber[msg.sender] = _myNumber;
}
function whatIsMyNumber(address _myaddress) public view returns (uint) {
return favoriteNumber[_myaddress];
}
}

如果我们想要在另一个合约中调用它,来调用者的数字
首先要定义一个接口,可以对比看一下,接口与合约不同之处在于我们之声明了要交互的函数而且没有函数体
编译器就是靠着这些特征识别出这个接口的
contract NumberInterface {
function whatIsMyNumber(address _myAddress) public view returns (uint);
}

使用接口
只要我们的合约是 public 或者 external,就可以与其他合约进行交互
NumberInterface numberContract = NumberInterface(LuckyNumber 合约的 Address);
// 现在变量 numberContract 指向另一个合约对象
uint num = numberContract.getNum(msg.sender);
//把我们的 favoriteNumber 赋值给 num

if 语句
if (keccak256(_name) == keccak256(“yichen”)) {
hello();
}

3

Ownable Contracts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
contract Ownable {
address public owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

//构造函数设置最初的所有者为合约的创建者
function Ownable() public {
owner = msg.sender;
}

//定义函数修饰符onlyOwner,函数修饰符只能添加到末尾来改变函数的行为
//如果所有者以外的其他任何帐户调用,则抛出该异常
modifier onlyOwner() {
require(msg.sender == owner);
_;
}

//允许当前所有者将合同的控制权转让给新的所有者
//newOwner是要将所有权转给的地址
function transferOwnership(address newOwner) public onlyOwner {
require(newOwner != address(0));
OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
}

如果一个函数添加了上面这个 onlyOwner 修饰符,调用这个函数的时候首先执行 onlyOwner 中的代码,执行到中的 _; 语句时,程序再返回并执行

上面提到的函数修饰符也可以像正常的函数一样去接受参数

1
2
3
4
5
6
7
8
9
10
11
// 存储用户年龄的映射
mapping (uint => uint) public age;
// 限定用户年龄的修饰符
modifier olderThan(uint _age, uint _userId) {
require(age[_userId] >= _age);
_;
}
// 必须年满18周岁才允许开车
// 我们可以用如下参数调用olderThan修饰符:
function driveCar(uint _userId) public olderThan(18, _userId) {
}

为什么要用 gas 来驱动?
以太坊就像一个巨大、缓慢、但非常安全的电脑。当你运行一个程序的时候,网络上的每一个节点都在进行相同的运算,以验证它的输出 —— 这就是所谓的“去中心化” 由于数以千计的节点同时在验证着每个功能的运行,这可以确保它的数据不会被被监控,或者被刻意修改
可能会有用户用无限循环堵塞网络,抑或用密集运算来占用大量的网络资源,为了防止这种事情的发生,以太坊的创建者为以太坊上的资源制定了价格,想要在以太坊上运算或者存储,你需要先付费,一次操作所需要花费的 gas 等于这个操作背后的所有运算花销的总和

payable 修饰符
使用这个修饰符使得函数可以接受以太币

提现
如果想在合约中提现,可以使用 owner.transfer(this.balance); 这样会把合约中的所有钱(this.balance)转给 owner