Solidity 合约

Solidity中合约有点类似面向对象语言中的类。合约中有用于数据持久化的状态变量(state variables),和可以操作他们的函数。调用另一个合约实例的函数时,会执行一个EVM函数调用,这个操作会切换执行时的上下文,这样,前一个合约的状态变量(state variables)就不能访问了。

合约可以通过Solidity,或不通过Solidity创建。当合约创建时,一个和合约同名的函数(构造器函数)会调用一次,用于初始化。

构造器函数是可选的。仅能有一个构造器,所以不支持重载。

如果不通过Solidity,我们可以通过web3.js,使用JavaScript的API来完成合约创建:

// Need to specify some source including contract name for the data param below
var source = "contract CONTRACT_NAME { function CONTRACT_NAME(unit a, uint b) {} }";
// The json abi array generated by the compiler
var abiArray = [
    {
        "inputs":[
            {"name":"x","type":"uint256"},
            {"name":"y","type":"uint256"}
        ],
        "type":"constructor"
    },
    {
        "constant":true,
        "inputs":[],
        "name":"x",
        "outputs":[{"name":"","type":"bytes32"}],
        "type":"function"
    }
];
var MyContract_ = web3.eth.contract(source);
MyContract = web3.eth.contract(MyContract_.CONTRACT_NAME.info.abiDefinition);
// deploy new contract
var contractInstance = MyContract.new(
    10,
    11,
    {from: myAccount, gas: 1000000}
);

具体内部实现里,构造器的参数是紧跟在合约代码的后面,但如果你使用web3.js,可以不用关心这样的细节。

如果一个合约要创建另一个合约,它必须要知道源码。这意味着循环创建依赖是不可能的。

pragma solidity ^0.4.0;
contract OwnedToken {
    // TokenCreator is a contract type that is defined below.
    // It is fine to reference it as long as it is not used
    // to create a new contract.
    TokenCreator creator;
    address owner;
    bytes32 name;
    // This is the constructor which registers the
    // creator and the assigned name.
    function OwnedToken(bytes32 _name) {
        // State variables are accessed via their name
        // and not via e.g. this.owner. This also applies
        // to functions and especially in the constructors,
        // you can only call them like that ("internall"),
        // because the contract itself does not exist yet.
        owner = msg.sender;
        // We do an explicit type conversion from `address`
        // to `TokenCreator` and assume that the type of
        // the calling contract is TokenCreator, there is
        // no real way to check that.
        creator = TokenCreator(msg.sender);
        name = _name;
    }
    function changeName(bytes32 newName) {
        // Only the creator can alter the name --
        // the comparison is possible since contracts
        // are implicitly convertible to addresses.
        if (msg.sender == address(creator))
            name = newName;
    }
    function transfer(address newOwner) {
        // Only the current owner can transfer the token.
        if (msg.sender != owner) return;
        // We also want to ask the creator if the transfer
        // is fine. Note that this calls a function of the
        // contract defined below. If the call fails (e.g.
        // due to out-of-gas), the execution here stops
        // immediately.
        if (creator.isTokenTransferOK(owner, newOwner))
            owner = newOwner;
    }
}
contract TokenCreator {
    function createToken(bytes32 name)
    returns (OwnedToken tokenAddress)
    {
        // Create a new Token contract and return its address.
        // From the JavaScript side, the return type is simply
        // "address", as this is the closest type available in
        // the ABI.
        return new OwnedToken(name);
    }
    function changeName(OwnedToken tokenAddress, bytes32 name) {
        // Again, the external type of "tokenAddress" is
        // simply "address".
        tokenAddress.changeName(name);
    }
    function isTokenTransferOK(
        address currentOwner,
        address newOwner
    ) returns (bool ok) {
        // Check some arbitrary condition.
        address tokenAddress = msg.sender;
        return (keccak256(newOwner) & 0xff) == (bytes20(tokenAddress) & 0xff);
    }
}

可见性或权限控制(Visibility And Accessors)

Solidity有两种函数调用方式,一种是内部调用,不会创建一个EVM调用(也叫做消息调用),另一种则是外部调用,会创建EVM调用(会发起消息调用)。Solidity对函数和状态变量提供了四种可见性。分别是external,public,internal,private。其中函数默认是public。状态变量默认的可见性是internal。

可见性

  • external: 外部函数是合约接口的一部分,所以我们可以从其它合约或通过交易来发起调用。一个外部函数f,不能通过内部的方式来发起调用,(如f()不可以,但可以通过this.f())。外部函数在接收大的数组数据时更加有效。
  • public: 公开函数是合约接口的一部分,可以通过内部,或者消息来进行调用。对于public类型的状态变量,会自动创建一个访问器(详见下文)。
  • internal: 这样声明的函数和状态变量只能通过内部访问。如在当前合约中调用,或继承的合约里调用。需要注意的是不能加前缀this,前缀this是表示通过外部方式访问。
  • private: 私有函数和状态变量仅在当前合约中可以访问,在继承的合约内,不可访问。

备注

所有在合约内的东西对外部的观察者来说都是可见,将某些东西标记为private仅仅阻止了其它合约来进行访问和修改,但并不能阻止其它人看到相关的信息。

可见性的标识符的定义位置,对于state variable是在类型后面,函数是在参数列表和返回关键字中间。来看一个定义的例子:

pragma solidity ^0.4.0;
contract C {
    function f(uint a) private returns (uint b) { return a + 1; }
    function setData(uint a) internal { data = a; }
    uint public data;
}

在下面的例子中,D可以调用c.getData()来访问data的值,但不能调用f。合约E继承自C,所以它可以访问compute函数。

pragma solidity ^0.4.0;
contract C {
    uint private data;
    function f(uint a) private returns(uint b) { return a + 1; }
    function setData(uint a) { data = a; }
    function getData() public returns(uint) { return data; }
    function compute(uint a, uint b) internal returns (uint) { return a+b; }
}
contract D {
    function readData() {
        C c = new C();
        uint local = c.f(7); // error: member "f" is not visible
        c.setData(3);
        local = c.getData();
        local = c.compute(3, 5); // error: member "compute" is not visible
    }
}
contract E is C {
    function g() {
        C c = new C();
        uint val = compute(3, 5);  // acces to internal member (from derivated to parent contract)
    }
}

访问函数(Getter Functions)

编译器为自动为所有的public的状态变量创建访问函数。下面的合约例子中,编译器会生成一个名叫data的无参,返回值是uint的类型的值data。状态变量的初始化可以在定义时完成。

pragma solidity ^0.4.0;
contract C{
    uint public c = 10;
}
contract D{
    C c = new C();
  
    function getDataUsingAccessor() returns (uint){
        return c.c();
    }
}

访问函数有外部(external)可见性。如果通过内部(internal)的方式访问,比如直接访问,你可以直接把它当一个变量进行使用,但如果使用外部(external)的方式来访问,如通过this.,那么它必须通过函数的方式来调用。

pragma solidity ^0.4.0;
contract C{
    uint public c = 10;
  
    function accessInternal() returns (uint){
        return c;
    }
  
    function accessExternal() returns (uint){
        return this.c();
    }
}

在acessExternal函数中,如果直接返回return this.c;,会出现报错Return argument type function () constant external returns (uint256) is not implicitly convertible to expected type (type of first return variable) uint256.。原因应该是通过外部(external)的方式只能访问到this.c作为函数的对象,所以它认为你是想把一个函数转为uint故而报错。

下面是一个更加复杂的例子:

pragma solidity ^0.4.0;
contract ComplexSimple{
    struct Cat{
        uint a;
        bytes3 b;
        mapping(uint => uint) map;
    }
  
    //
    mapping(uint => mapping(bool => Cat)) public content;
  
    function initial(){
        content[0][true] = Cat(1, 1);
        content[0][true].map[0] = 10;
    }
  
    function get() returns (uint, bytes3, uint){
        return (content[0][true].a, content[0][true].b, content[0][true].map[0]);
    }
}
contract Complex {
    struct Data {
        uint a;
        bytes3 b;
        mapping (uint => uint) map;
    }
    mapping (uint => mapping(bool => Data[])) public data;
  
  
}

文档中自带的的这个Demo始终跑不通,数组类型这里不知为何会抛invalid jump。把这块简化了写了一个ComplextSimple供参考。

需要注意的是public的mapping默认访问参数是需要参数的,并不是之前说的访问函数都是无参的。

mapping类型的数据访问方式变为了data[arg1][arg2][arg3].a

结构体(struct)里的mapping初始化被省略了,因为并没有一个很好的方式来对键赋值。

函数修改器(Function Modifiers)

修改器(Modifiers)可以用来轻易的改变一个函数的行为。比如用于在函数执行前检查某种前置条件。修改器是一种合约属性,可被继承,同时还可被派生的合约重写(override)。下面我们来看一段示例代码:

pragma solidity ^0.4.0;
contract owned {
    function owned() { owner = msg.sender; }
    address owner;
    // This contract only defines a modifier but does not use
    // it - it will be used in derived contracts.
    // The function body is inserted where the special symbol
    // "_;" in the definition of a modifier appears.
    // This means that if the owner calls this function, the
    // function is executed and otherwise, an exception is
    // thrown.
    modifier onlyOwner {
        if (msg.sender != owner)
            throw;
        _;
    }
}
contract mortal is owned {
    // This contract inherits the "onlyOwner"-modifier from
    // "owned" and applies it to the "close"-function, which
    // causes that calls to "close" only have an effect if
    // they are made by the stored owner.
    function close() onlyOwner {
        selfdestruct(owner);
    }
}
contract priced {
    // Modifiers can receive arguments:
    modifier costs(uint price) {
        if (msg.value >= price) {
            _;
        }
    }
}
contract Register is priced, owned {
    mapping (address => bool) registeredAddresses;
    uint price;
    function Register(uint initialPrice) { price = initialPrice; }
    // It is important to also provide the
    // "payable" keyword here, otherwise the function will
    // automatically reject all Ether sent to it.
    function register() payable costs(price) {
        registeredAddresses[msg.sender] = true;
    }
    function changePrice(uint _price) onlyOwner {
        price = _price;
    }
}

修改器可以被继承,使用将modifier置于参数后,返回值前即可。

特殊_表示使用修改符的函数体的替换位置。

合约Register可以看出全约可以多继承,通过,号分隔两个被继承的对象。

修改器也是可以接收参数的,如priced的costs。

使用修改器实现的一个防重复进入的例子。

pragma solidity ^0.4.0;
contract Mutex {
    bool locked;
    modifier noReentrancy() {
        if (locked) throw;
        locked = true;
        _;
        locked = false;
    }
    /// This function is protected by a mutex, which means that
    /// reentrant calls from within msg.sender.call cannot call f again.
    /// The `return 7` statement assigns 7 to the return value but still
    /// executes the statement `locked = false` in the modifier.
    function f() noReentrancy returns (uint) {
        if (!msg.sender.call()) throw;
        return 7;
    }
}

例子中,由于call()方法有可能会调回当前方法,修改器实现了防重入的检查。

如果同一个函数有多个修改器,他们之间以空格隔开,修饰器会依次检查执行。

需要注意的是,在Solidity的早期版本中,有修改器的函数,它的return语句的行为有些不同。

在修改器中和函数体内的显式的return语句,仅仅跳出当前的修改器和函数体。返回的变量会被赋值,但整个执行逻辑会在前一个修改器后面定义的"_"后继续执行。

修改器的参数可以是任意表达式。在对应的上下文中,所有的函数中引入的符号,在修改器中均可见。但修改器中引入的符号在函数中不可见,因为它们有可能被重写。

常量(constant state variables)

状态变量可以被定义为constant,常量。这样的话,它必须在编译期间通过一个表达式赋值。赋值的表达式不允许:1)访问storage;2)区块链数据,如now,this.balance,block.number;3)合约执行的中间数据,如msg.gas;4)向外部合约发起调用。也许会造成内存分配副作用表达式是允许的,但不允许产生其它内存对象的副作用的表达式。内置的函数keccak256,keccak256,ripemd160,ecrecover,addmod,mulmod可以允许调用,即使它们是调用的外部合约。

允许内存分配,从而带来可能的副作用的原因是因为这将允许构建复杂的对象,比如,查找表。虽然当前的特性尚未完整支持。

编译器并不会为常量在storage上预留空间,每个使用的常量都会被对应的常量表达式所替换(也许优化器会直接替换为常量表达式的结果值)。

不是所有的类型都支持常量,当前支持的仅有值类型和字符串。

pragma solidity ^0.4.0;
contract C {
    uint constant x = 32**22 + 8;
    string constant text = "abc";
    bytes32 constant myHash = keccak256("abc");
}

常函数(Constant Functions)

函数也可被声明为常量,这类函数将承诺自己不修改区块链上任何状态。

pragma solidity ^0.4.0;
contract C {
    function f(uint a, uint b) constant returns (uint) {
        return a * (b + 42);
    }
}

访问器(Accessor)方法默认被标记为constant。当前编译器并未强制一个constant的方法不能修改状态。但建议大家对于不会修改数据的标记为constant。

回退函数(fallback function)

每一个合约有且仅有一个没有名字的函数。这个函数无参数,也无返回值。如果调用合约时,没有匹配上任何一个函数(或者没有传哪怕一点数据),就会调用默认的回退函数。

此外,当合约收到ether时(没有任何其它数据),这个函数也会被执行。在此时,一般仅有少量的gas剩余,用于执行这个函数(准确的说,还剩2300gas)。所以应该尽量保证回退函数使用少的gas。

下述提供给回退函数可执行的操作会比常规的花费得多一点。

  • 写入到存储(storage)
  • 创建一个合约
  • 执行一个外部(external)函数调用,会花费非常多的gas
  • 发送ether

请在部署合约到网络前,保证透彻的测试你的回退函数,来保证函数执行的花费控制在2300gas以内。

一个没有定义一个回退函数的合约。如果接收ether,会触发异常,并返还ether(solidity v0.4.0开始)。所以合约要接收ether,必须实现回退函数。下面来看个例子:

pragma solidity ^0.4.0;
contract Test {
    // This function is called for all messages sent to
    // this contract (there is no other function).
    // Sending Ether to this contract will cause an exception,
    // because the fallback function does not have the "payable"
    // modifier.
    function() { x = 1; }
    uint x;
}
// This contract keeps all Ether sent to it with no way
// to get it back.
contract Sink {
    function() payable { }
}
contract Caller {
    function callTest(Test test) {
        test.call(0xabcdef01); // hash does not exist
        // results in test.x becoming == 1.
        // The following call will fail, reject the
        // Ether and return false:
        test.send(2 ether);
    }
}

在浏览器中跑的话,记得要先存ether。

事件(Events)

事件是使用EVM日志内置功能的方便工具,在DAPP的接口中,它可以反过来调用JavaScript的监听事件的回调。

事件在合约中可被继承。当被调用时,会触发参数存储到交易的日志中(一种区块链上的特殊数据结构)。这些日志与合约的地址关联,并合并到区块链中,只要区块可以访问就一直存在(至少Frontier,Homestead是这样,但Serenity也许也是这样)。日志和事件在合约内不可直接被访问,即使是创建日志的合约。

日志的SPV(简单支付验证)是可能的,如果一个外部的实体提供了一个这样证明的合约,它可以证明日志在区块链是否存在。但需要留意的是,由于合约中仅能访问最近的256个区块哈希,所以还需要提供区块头信息。

可以最多有三个参数被设置为indexed,来设置是否被索引。设置为索引后,可以允许通过这个参数来查找日志,甚至可以按特定的值过滤。

如果数组(包括string和bytes)类型被标记为索引项,会用它对应的Keccak-256哈希值做为topic。

除非是匿名事件,否则事件签名(比如:Deposit(address,hash256,uint256))是其中一个topic,同时也意味着对于匿名事件无法通过名字来过滤。

所有未被索引的参数将被做为日志的一部分被保存起来。

被索引的参数将不会保存它们自己,你可以搜索他们的值,但不能检索值本身。

下面是一个简单的例子:

pragma solidity ^0.4.0;
contract ClientReceipt {
    event Deposit(
        address indexed _from,
        bytes32 indexed _id,
        uint _value
    );
    function deposit(bytes32 _id) {
        // Any call to this function (even deeply nested) can
        // be detected from the JavaScript API by filtering
        // for `Deposit` to be called.
        Deposit(msg.sender, _id, msg.value);
    }
}

下述是使用JavaScript来获取日志的例子。

var abi = /* abi as generated by the compiler */;
var ClientReceipt = web3.eth.contract(abi);
var clientReceipt = ClientReceipt.at(0x123 /* address */);
var event = clientReceipt.Deposit();
// watch for changes
event.watch(function(error, result){
    // result will contain various information
    // including the argumets given to the Deposit
    // call.
    if (!error)
        console.log(result);
});
// Or pass a callback to start watching immediately
var event = clientReceipt.Deposit(function(error, result) {
    if (!error)
        console.log(result);
});

底层的日志接口(Low-level Interface to Logs) 通过函数log0,log1,log2,log3,log4,可以直接访问底层的日志组件。logi表示总共有带i + 1个参数(i表示的就是可带参数的数目,只是是从0开始计数的)。其中第一个参数会被用来做为日志的数据部分,其它的会做为主题(topics)。前面例子中的事件可改为如下:

log3(
    msg.value,
    0x50cb9fe53daa9737b786ab3646f04d0150dc50ef4e75f59509d83667ad5adb20,
    msg.sender,
    _id
);

其中的长16进制串是事件的签名,计算方式是keccak256("Deposit(address,hash256,uint256)")

继承(Inheritance)

Solidity通过复制包括多态的代码来支持多重继承。

所有函数调用是虚拟(virtual)的,这意味着最远的派生方式会被调用,除非明确指定了合约

当一个合约从多个其它合约那里继承,在区块链上仅会创建一个合约,在父合约里的代码会复制来形成继承合约。

基本的继承体系与python有些类似,特别是在处理多继承上面。

下面用一个例子来详细说明:

pragma solidity ^0.4.0;
contract owned {
    function owned() { owner = msg.sender; }
    address owner;
}
// Use "is" to derive from another contract. Derived
// contracts can access all non-private members including
// internal functions and state variables. These cannot be
// accessed externally via `this`, though.
contract mortal is owned {
    function kill() {
        if (msg.sender == owner) selfdestruct(owner);
    }
}
// These abstract contracts are only provided to make the
// interface known to the compiler. Note the function
// without body. If a contract does not implement all
// functions it can only be used as an interface.
contract Config {
    function lookup(uint id) returns (address adr);
}
contract NameReg {
    function register(bytes32 name);
    function unregister();
}
// Multiple inheritance is possible. Note that "owned" is
// also a base class of "mortal", yet there is only a single
// instance of "owned" (as for virtual inheritance in C++).
contract named is owned, mortal {
    function named(bytes32 name) {
        Config config = Config(0xd5f9d8d94886e70b06e474c3fb14fd43e2f23970);
        NameReg(config.lookup(1)).register(name);
    }
    // Functions can be overridden by another function with the same name and
    // the same number/types of inputs.  If the overriding function has different
    // types of output parameters, that causes an error.
    // Both local and message-based function calls take these overrides
    // into account.
    function kill() {
        if (msg.sender == owner) {
            Config config = Config(0xd5f9d8d94886e70b06e474c3fb14fd43e2f23970);
            NameReg(config.lookup(1)).unregister();
            // It is still possible to call a specific
            // overridden function.
            mortal.kill();
        }
    }
}
// If a constructor takes an argument, it needs to be
// provided in the header (or modifier-invocation-style at
// the constructor of the derived contract (see below)).
contract PriceFeed is owned, mortal, named("GoldFeed") {
function updateInfo(uint newInfo) {
    if (msg.sender == owner) info = newInfo;
}
function get() constant returns(uint r) { return info; }
uint info;
}
上面的例子的named合约的kill()方法中,我们调用了motal.kill()调用父合约的销毁函数(destruction)。但这样可能什么引发一些小问题。
pragma solidity ^0.4.0;
contract mortal is owned {
    function kill() {
        if (msg.sender == owner) selfdestruct(owner);
    }
}
contract Base1 is mortal {
    function kill() { /* do cleanup 1 */ mortal.kill(); }
}
contract Base2 is mortal {
    function kill() { /* do cleanup 2 */ mortal.kill(); }
}
contract Final is Base1, Base2 {
}
对Final.kill()的调用只会调用Base2.kill(),因为派生重写,会跳过Base1.kill,因为它根本就不知道有Base1。一个变通方法是使用super。
pragma solidity ^0.4.0;
contract mortal is owned {
    function kill() {
        if (msg.sender == owner) selfdestruct(owner);
    }
}
contract Base1 is mortal {
    function kill() { /* do cleanup 1 */ super.kill(); }
}
contract Base2 is mortal {
    function kill() { /* do cleanup 2 */ super.kill(); }
}
contract Final is Base2, Base1 {
}

如果Base1调用了函数super,它不会简单的调用基类的合约函数,它还会调用继承关系图谱上的下一个基类合约,所以会调用Base2.kill()。需要注意的最终的继承图谱将会是:Final,Base1,Base2,mortal,owned。使用super时会调用的实际函数在使用它的类的上下文中是未知的,尽管它的类型是已知的。这类似于普通虚函数查找(ordinary virtual method lookup)

基类构造器的方法(Arguments for Base Constructors)

派生的合约需要提供所有父合约需要的所有参数,所以用两种方式来做,见下面的例子:

pragma solidity ^0.4.0;
contract Base {
    uint x;
    function Base(uint _x) { x = _x; }
}
contract Derived is Base(7) {
    function Derived(uint _y) Base(_y * _y) {
    }
}

或者直接在继承列表中使用is Base(7),或像修改器(modifier)使用方式一样,做为派生构造器定义头的一部分Base(_y * _y)。第一种方式对于构造器是常量的情况比较方便,可以大概说明合约的行为。第二种方式适用于构造的参数值由派生合约的指定的情况。在上述两种都用的情况下,第二种方式优先(一般情况只用其中一种方式就好了)。

多继承与线性化(Multiple Inheritance and Linearization) 实现多继承的编程语言需要解决几个问题,其中之一是菱形继承问题又称钻石问题,如下图。

Solidity的解决方案参考Python,使用C3_linearization来强制将基类合约转换一个有向无环图(DAG)的特定顺序。结果是我们希望的单调性,但却禁止了某些继承行为。特别是基类合约在is后的顺序非常重要。下面的代码,Solidity会报错Linearization of inheritance graph impossible。

pragma solidity ^0.4.0;
contract X {}
contract A is X {}
contract C is A, X {}

原因是C会请求X来重写A(因为继承定义的顺序是A,X),但A自身又是重写X的,所以这是一个不可解决的矛盾。

一个简单的指定基类合约的继承顺序原则是从most base-like到most derived。

继承有相同名字的不同类型成员

当继承最终导致一个合约同时存在多个相同名字的修改器或函数,它将被视为一个错误。同新的如果事件与修改器重名,或者函数与事件重名都将产生错误。作为一个例外,状态变量的getter可以覆盖一个public的函数。

抽象(Abstract Contracts)

抽象函数是没有函数体的的函数。如下:

pragma solidity ^0.4.0;
contract Feline {
    function utterance() returns (bytes32);
}

这样的合约不能通过编译,即使合约内也包含一些正常的函数。但它们可以做为基合约被继承。

pragma solidity ^0.4.0;
contract Feline {
    function utterance() returns (bytes32);
  
    function getContractName() returns (string){
        return "Feline";
    }
}
contract Cat is Feline {
    function utterance() returns (bytes32) { return "miaow"; }
}

如果一个合约从一个抽象合约里继承,但却没实现所有函数,那么它也是一个抽象合约。

接口

接口与抽象合约类似,与之不同的是,接口内没有任何函数是已实现的,同时还有如下限制:

  • 不能继承其它合约,或接口。
  • 不能定义构造器
  • 不能定义变量
  • 不能定义结构体
  • 不能定义枚举类

其中的一些限制可能在未来放开。

接口基本上限制为合约ABI定义可以表示的内容,ABI和接口定义之间的转换应该是可能的,不会有任何信息丢失。

接口用自己的关键词表示:

interface Token {
    function transfer(address recipient, uint amount);
}

合约可以继承于接口,因为他们可以继承于其它的合约。