solidity学习笔记

overview

solidity是为了以太坊智能合约而创造的语言,语言本身并不复杂,有其他编程语言基础的人应该很容易上手,
但是由于其特殊的运行环境,它也有一些独有的特性。
先看一段智能合约代码,这是来自官方文档的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pragma solidity ^0.4.21; //合约的第一句必须是这个形式,表明solidity版本,当前最新版本是0.4.24

contract Coin { //用contract申明这个合约,Coin是合约名称,这里类似于c++中的class申明
address public minter; //变量申明,address是数据类型,public是访问权限,minter是变量名
mapping (address => uint) public balances; // 变量申明,mapping相当于c++里的map, python的字典

events Sent(address from, address to, uint amount); // 事件申明

constructor public { //构造函数
minter = msg.sender;
}

function mint(address receiver, uint amount) public { // 普通函数,mint是函数名
if(msg.sender != minter) return;
balances[receiver] += amount;
}

function send(address receiver, uint amount) public {
if(balances[msg.sender] < amount) return;
balances[msg.sender] -= amount;
balances[receiver] += amount;
emit Sent(msg.sender, receiver, amount); //向全网发送一个消息,内容是Sent中的三个参数
}
}

这段代码展示了一个智能合约的基本结构,代码中体现了solidity的一些基本特性。
一个solidity合约就相当于其他面向对象语言的一个类,同样有属性和方法。
这里先对solidity写一个合约有个整体性了解,接下来再细说一些具体细节。

memory\storage

  • 这两个关键字通常用来修饰变量
  • memory是存储在内存中的,storage是存储在链上的,也就是memory的变量在作用域结束后就消失,
    storage变量永久保存。
  • 合约的状态变量默认是storage,函数局部变量和函数传参默认是memory。
  • 在有写情况需要显示声明变量的存储位置,storage像是指针,而memory则是新开辟空间。

constant\view\pure关键字

这三个关键字用于控制function的读写权限,类似于c++中类方法的const的位置。

关键字的位置
1
function func(arg1...) view public { ... }
constant和view

这两个关键字的作用是一样的,都是用于限制function对合约的内容只有只读权限,不能修改合约状态。
新版本的solidity用view取代了constant。

pure

表示所修饰的function对合约的状态不读不写,给我的感觉有点像c++里面的static函数或者全局函数。
比如:

1
2
3
4
function func(uint a, uint b) pure public returns(uint) {
uint c = a + b;
return c;
}

为什么会有这些限制

首先,在以太坊上进行任何一笔交易,都是需要消耗gas的,对应的也就是以太币,而且需要全网验证、矿工挖矿。
那么用pure和view修饰的函数,只是从某个节点读取数据(甚至不读不写),不需要产生交易,所以可以省钱省时间。
如果一个函数没有用view和pure修饰,默认是要产生一笔交易的,即使函数不对合约状态做任何修改。
(至于EVM是如何处理的还没有深究)。
此外,如果产生一笔交易,花费很长时间,并且要全网验证同步,函数中的返回值是不会直接能读出的,即使在函数中
写明有返回值,在调用的时候返回的也只有交易的信息。

可见性

solidity的可见性有四类:external\public\internal\private

  • external,合约接口的一部分,只能在合约外部调用,比如别的合约、客户端。
  • public,合约接口的一部分,类似其他面向对象语言的public。
  • internal,类似于c++的protected。
  • private,类似于c++的private。
    solidity的可见性也要尽量按照函数应该有的访问权限写好,因为不同访问权限的函数对应的gas消耗不一样,
    这与EVM的实现有关(具体也还要继续研究)

modifier

翻译成中文是修饰器,类似于python的装饰器。
一段简单的示例如下:

1
2
3
4
5
6
7
8
modifier need() {
require(condition == true);
_;
}

function func(arg...) modifier {

}

以上的代码,在func中添加了一个修饰器,函数被调用时会先执行modifier定义的内容,再执行自己本身的内容。

关键字assert\require\revert

这几个关键字用于代替0.4.10以前的

1
if(!condition) { throw;}

现在的写法是:

1
2
3
assert(condition);
require(condition);
if(!condition) { revert();}

以下是assert和require的一点区别,还有一些问题暂时没弄懂:

  • assert会终止程序,并消耗掉剩余的gas,.
  • require会终止程序,并返回剩余的gas.

solidity自带的特殊变量和函数

solidity中自带了一些内置函数和变量,如下:

  • block.blockhash(uint blockNumber) returns (byte32)–返回指定blockNumber的哈希值(仅限最近的256个block,并且
    不包括当前block)
  • blockhash(uint blockNumber)–在0.4.22后替代block.blockhash
  • block.coinbase (address)–当前区块的矿工地址
  • block.difficulty (uint)–当前区块的挖矿难度
  • block.gaslimit (uint)–当前区块的gaslimit
  • block.number (uint)–当前区块的高度
  • block.timestamp (uint)–当前区块的时间戳
  • gasleft() returns (uint256)–还剩下的gas
  • msg.data (bytes)–complete calldata(不知道什么意思)
  • msg.gas (uint)–0.4.21后被gasleft代替
  • msg.sender (address)–发起交易的地址
  • msg.sig (bytes4)–first four bytes of the calldata
  • msg.value (uint)–number of wei sent with the message
  • now (uint)–当前时间戳,同block.timestamp
  • tx.gasprice (uint)–当前这笔交易的燃料价格
  • tx.origin (address)–当前这笔交易的发起者

其他

solidity的语言细节和于EVM相关的机制内容还有很多,这里仅仅简要的记录了一小部分。
另外,在学习的过程中,体会到一边看别人的代码一边写,遇到不知道的再去查文档是一个比较有效的方式。
这里推荐看一下
加密的源代码
代码量并不多,其中涉及的语言细节上面基本上都提到了,而且看懂加密猫的代码
也就能自己写一些合约了。


增加的

fallback函数

  • fallback函数是合约中一个特殊的函数,没有返回值,没有参数,没有函数名,一个合约中只能有一个。在合约调用没有匹配
    到函数签名,或者调用没有携带任何数据时被自动调用。
  • solidity提供了编译期检查,所有不能调用不存在的函数,但是可以通过底层的address.call来模拟。
  • 直接上代码分析吧:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    pragma solidity ^0.4.0;

    contract ExecuteFallback{

    //回退事件,会把调用的数据打印出来
    event FallbackCalled(bytes data);
    //fallback函数,注意是没有名字的,没有参数,没有返回值的
    function(){
    FallbackCalled(msg.data);
    }

    //调用已存在函数的事件,会把调用的原始数据,请求参数打印出来
    event ExistFuncCalled(bytes data, uint256 para);
    //一个存在的函数
    function existFunc(uint256 para){
    ExistFuncCalled(msg.data, para);
    }

    // 模拟从外部对一个存在的函数发起一个调用,将直接调用函数
    function callExistFunc(){
    bytes4 funcIdentifier = bytes4(keccak256("existFunc(uint256)"));
    this.call(funcIdentifier, uint256(1));
    }

    //模拟从外部对一个不存在的函数发起一个调用,由于匹配不到函数,将调用回退函数
    function callNonExistFunc(){
    bytes4 funcIdentifier = bytes4(keccak256("functionNotExist()"));
    this.call(funcIdentifier);
    }
    }
  • solidity函数签名方式–bytes4(keccak256(“existFunc(uint256)”)),引号中间是函数名和参数

  • msg.data–函数签名加上参数
  • address.call调用方式–底层通过地址调用一个合约方法,第一个参数是函数签名,之后是参数。

send

  • 使用address.send(ether to send)向某个合约直接转以太币,由于这个行为没有发送任何数据,
    所以接收合约总是会调用fallback函数。
  • 示例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    pragma solidity ^0.4.0;

    contract SendFallback{

    //fallback函数及其事件
    event fallbackTrigged(bytes data);
    function() payable{fallbackTrigged(msg.data);}

    //存入一些ether用于后面的测试
    function deposit() payable{
    }

    //查询当前的余额
    function getBalance() constant returns(uint){
    return this.balance;
    }

    event SendEvent(address to, uint value, bool result);
    //使用send()发送ether,观察会触发fallback函数
    function sendEther(){
    bool result = this.send(1);
    SendEvent(this, 1, result);
    }
    }
  • 如果我们要在合约中通过send()函数接收,就必须定义fallback函数,否则会抛异常。

  • fallback函数必须增加payable关键字,否则send()执行结果将会始终为false。
  • fallback的限制,send函数会限制gas的数量,防止恶意的操作。

payable

  • 用于标识一个方法,在调用时可以接受以太币。
  • 调用方式–address.call(some method).value(ether account).
  • msg.value得到的是随调用指定的以太币数量。

call

  • 通过底层的方式调用指定地址的方法。
  • addr.call(“abc”, 256),abc是函数名称,256是对应的参数
  • bytes4 methodId = bytes4(keccak256(“increaseAge(string,uint256)”));addr.call(methodId,”jack”, 1);
    如果第一个参数刚好是四个字节,会认为这四个字节指定的是函数签名的序号值,
    由如果你只是想传个参数值,而不是想指定一个函数序号,应避免第一个参数刚好是四个字节

delegatecall

  • 功能与call类似
  • 区别在于运行环境不通,delegatecall是在调用的函数是在当前环境下运行,call调用的函数是在被调用环境运行。
    delegatecall相当于将目标代码放到当前环境运行。