Solidity

  • Post author:
  • Post category:solidity




Solidity简介

Solidity是一门面向合约的高级编程语言。该语言设计的目的是能在 以太坊虚拟机(EVM)上运行。

Solidity是静态类型语言,支持继承、库 和 复杂的用户定义类型等特性。

目前尝试Solidity编程的最好方式是

Remix



Pragmas

关键字 pragma 版本标识指令,用来启用某些编译器检查。

版本 标识pragma 指令通常只对

本文件

有效,所以pragma要写到所有源文件里。

使用import导入其他文件, 其标识pragma不会从跟着被导入。

pragma solidity ^0.5.2;

不允许低于 0.5.2 也不允许高于(包含) 0.6.0 版本的编译器编译(第二个条件因使用 ^ 被添加)。

可以使用更复杂的规则来指定编译器的版本,表达式遵循

npm 版本语义

pragma solidity >=0.4.22 < 0.8.0;



导入其他源文件

Solidity 支持的导入语句来模块化代码,其语法跟 JavaScript(从 ES6 起)非常类似。

// 不推荐
import "filename";

// 推荐, 使用时直接 symbolName.***
import * as symbolName from "filename";
import {aaa as alias, bbb} from "filename";



注释

// 这是一个单行注释。

/*
这是一个
多行注释。
*/



合约结构

在 Solidity 语言中,把定义合约 的 contract 改成 class 就是 其他面向对象编程语言中的





每个合约中可以包含 状态变量、 函数、 函数 , 事件 Event, 错误(Errors), 结构体 和 枚举类型 的声明,且 合约可以从 其他合约 继承。



状态变量

状态变量是

永久存储

在 合约 中的 值。

pragma solidity >=0.4.0 <0.9.0;

contract TinyStorage {
    uint storedXlbData; // 状态变量
    // ...
}



函数

函数通常在合约内部定义,但也可以在合约外定义。

函数调用 可发生在合约内部或外部。

pragma solidity ^0.5.2;

contract Wtt {
    // 定义函数
    function Aaa() public payable { 
        // ...
    }
}

function bbb(uint x) pure returns (uint) {
    return x * 2;
}



事件

contract Wtt {
    event AaaBbb(address bidder, uint amount); // 定义事件

    function bid() public payable {
        // ...
        emit AaaBbb(msg.sender, msg.value); // 触发事件
    }
}



错误

error NotEnoughFunds(uint requested, uint available); // 定义错误

contract Token {
    function aaa (address to, uint amount) public {
        if (balance < amount)
            revert NotEnoughFunds(amount, balance); // 使用revert激发 错误
    }
}



结构体

contract Wtt {
    struct Who { // 结构体
        uint age;
        bool gender;
        address delegate;
    }
}



枚举

contract Upchain {
    enum State { Zero, One, Two, Three } // 枚举
}



类型

Solidity 是一种静态类型语言,这意味着每个变量都需要在 编译时 指定变量的类型。

“undefined”或“null”值的概念在Solidity中不存在,但是新声明的变量总是有一个 默认值 ,具体的默认值跟类型相关。



值类型

这些类型的变量将始终按值来传递。 也就是说,当这些变量被用作函数参数或者用在赋值语句中时,总会进行值拷贝。



布尔(bool)

bool aaa = true;

/*
运算符:
        ! (逻辑非)
        && (逻辑与, “and” )
        || (逻辑或, “or” )
        == (等于)
        != (不等于)
*/



整型(int、uint)

int / uint :分别表示有符号和无符号的不同位数的整型变量。

支持关键字 uint8 到 uint256 (无符号,从 8 位到 256 位),以 8 位为步长递增。

uint 和 int 分别是 uint256 和 int256 的 别名。

int aaa = 123;


范围检查



Solidity中的整数是有取值范围的。

例如 uint32 类型的取值范围是 0 到 2**(32-1)

0.8.0 开始,算术运算有两个计算模式:

  1. “wrapping”(截断)模式 或称 “unchecked”(不检查)模式
  2. ”checked” (检查)模式

默认情况下采用 “checked” 模式下,即都会进行溢出检查,如果结果落在取值范围之外,调用会通过 失败异常 回退。

也可以通过 unchecked { … } 切换到 “unchecked”模式



定长浮点型(fixed、ufixed)

在关键字 fixedMxN 中,M表示小数总的位数,N表示小数点后的位数。

M 必须能整除 8,即 8 到 256 中所有8的倍数。

N 则可以是从 0 到 80 之间的任意数。

fixed8x2 aaa = 300000.14



地址(address)

这个类型 放到后面再说



定长字节数组

关键字有: bytes1、bytes2、…、bytes32



枚举(enum)

从0开始递增。

enum State { Zero, One, Two, Three };



引用类型

引用类型可以通过

多个不同的名称

修改它的值,

而值类型的变量,每次都有独立的副本。

因此,必须比值类型更谨慎地处理引用类型。

如果使用 引用类型,则 必须明确指明 数据

存储位置


  • 内存memory

    :即数据在内存中,因此数据仅在其生命周期内(函数调用期间)有效。不能用于外部调用。

  • 存储storage

    : 状态变量保存的位置,只要合约存在就一直存储.

  • 调用数据calldata

    : 用来保存 函数参数 的特殊数据位置,是一个 只读位置,不可编辑修改。

更改

数据位置 或 类型转换

都会自动进行一份拷贝。

数据位置与赋值行为 有以下情况:

  • 在 存储storage 和 内存memory 之间相互赋值,会创建一份独立的拷贝。
  • 从 存储storage 到 存储storage 的赋值 只 创建引用
  • 从 内存memory 到 内存memory 的赋值 只 创建引用
  • 其他的 向 存储storage 的赋值,总是进行拷贝。



数组(Array)

数组可以在声明时指定长度(定长数组),也可以动态调整大小(动态数组)。

uint[5] aaa;

int[] bbb;

数组元素可以是任何类型,但是 对于 内存型 的 数组,其元素不能是 Mapping类型。

访问超出 数组长度 的 元素 会导致异常。

  • 创建 内存数组

可使用

new 关键字

在 内存memory 中 创建 动态长度数组。

该动态数组 要区分于 存储storage 数组,不能通过.push改变 内存数组 的大小。

所以 必须提前计算好 所需的 大小 再创建内存数组。

uint[] memory x = new uint[](3);
x[0] = 1;
x[1] = 3;
x[2] = 4;

需要注意的是,定长的 内存memory 数组并不能赋值给变长的 内存memory 数组

  • 数组成员
  • length:

    表示当前数组的长度。
  • push():

    添加新的 零初始化 元素 到数组末尾,并返回元素引用
  • pop():

    用来从数组末尾删除元素。
  • 数组切片

数组切片是数组连续部分的视图,用法如:x[start:end]。

x[start:end] 的第一个元素是 x[start] , 最后一个元素是 x[end – 1] 。

如果 start 比 end 大或者 end 比数组长度还大,将会抛出异常。

start 和 end 都可以是可选的: start 默认是 0, 而 end 默认是数组长度。

数组切片没有 类型名称,这意味着没有变量可以将数组切片作为类型,它们仅存在于中间表达式中。



bytes 和 string 也是数组

bytes 和 string 类型的变量是特殊的数组,但 不允许 用 长度 或 索引 来访问。



结构体(Struct)

通过构造结构体的形式定义新的类型。

结构体 可以 作为 mapping 和 数组 的 元素成员,

但它并不能包含自身。 这个限制是有必要的,因为结构体的 大小 必须是 有限的。



映射(Mapping)

KeyType 可以是 任何基本类型,

ValueType 可以 任何类型。

映射是没有长度的,映射只能是 存储storage 的 数据位置,

mapping(address => uint) public balances;
  • 可迭代映射

映射本身是无法遍历的,即无法枚举所有的键。

不过,可以在它们之上实现一个数据结构来进行迭代。



地址类型

地址类型有两种形式:



address

保存一个20字节的值(以太坊地址的大小)。



address payable

可支付地址,与 address 相同,不过有成员函数 transfer 和 send 。

这种区别背后的思想是 可以向 address payable 发送以太币,

而不能向一个普通的 address 发送以太币,

比如,address可能是一个智能合约地址,并且不支持接收以太币。

  • 类型转换

允许从 address payable 到 address 的隐式转换,

而从 address 到 address payable 必须显示的转换, 通过

payable(<address>)

.

  • 地址类型 成员变量
  • .balance(uint256)

查询该地址的 ether余额,单位是Wei。

  • .transfer(uint256 amount)

向指定 地址 发送 数量为 amount的 ether(单位wei),

失败时 抛出异常,发送2300gas的矿工费,不可调节。

  • .send(uint256 amount) returns (bool)

send 是 transfer 的低级版本。向指定 地址 发送 数量为 amount的 ether(单位wei),

失败时 返回false,发送2300gas的矿工费,不可调节。

  • .call(bytes memory) returns (bool,bytes memory)

发出底层函数CALL,失败是 返回 false,发送所有可用 gas,可以调节。

  • .delegatecall(bytes memory) returns (bool,bytes memory)

发出底层函数delegatecall,失败是 返回 false,发送所有可用 gas,可以调节。

  • .staticcall(bytes memory) returns (bool,bytes memory)

发出底层函数staticcall,失败是 返回 false,发送所有可用 gas,可以调节。

函数 call , delegatecall 和 staticcall 都是非常低级的函数,应该只把它们当作 最后一招 来使用,因为它们破坏了 Solidity 的类型安全性。



类型转换



隐式转换

在某些情况下,编译器会自动进行隐式类型转换, 这些情况包括: 在赋值, 参数传递给函数以及应用运算符时。

通常,如果可以进行值类型之间的隐式转换, 并且不会丢失任何信息。 都是可以隐式类型转换

uint8 y;
uint16 z;
uint32 x = y + z;



显式转换

如果某些情况下编译器不支持隐式转换,但是你很清楚你要做的结果,这种情况可以考虑显式转换。 注意这可能会发生一些无法预料的后果,因此一定要进行测试,确保结果是你想要的!

int8 y = -3;
uint x = uint(y);

如果一个类型显式转换成更小的类型,相应的高位将被舍弃

uint32 a = 0x12345678;
uint16 b = uint16(a); // 此时 b 的值是 0x5678

如果将整数显式转换为更大的类型,则将填充左侧(即在更高阶的位置)

uint16 a = 0x1234;
uint32 b = uint32(a); // b 为 0x00001234 now
assert(a == b);



数据位置

所有的 引用类型 都有一个 额外属性 即 “数据位置描述”, 用来说明 数据是保存在 内存memory中 还是 存储storage中。

在不同的上下文中,数据 有默认的 位置,但是也可以在 类型 后面 增加关键字

storage



memory

进行修改。

  • 函数参数:默认是 memory
  • 引用类型 局部变量:默认是 storage,有点 反人类
  • 值类型 局部变量:默认是 stack 栈
  • 状态变量:即合约属性,强制是 storage。


特别要求


public公开可见的函数 的 参数 一定是 memory类型。

如果参数要求必须用 storage类型,则函数 应该置为 private 或 internal,这是为了防止 随意调用占用资源。


举例:

function aaa(uint[] storage arr) public {
    arr.push(123);
}

公开函数 public 的参数 一定不能是 storage空间 中的变量的话,否则 别人 随便 调用的话, 会造成 storage空间 的浪费。

所以 如果 参数一定要是 storage空间 中的变量的话,

函数 应该设定为 私有函数 如下:

function aaa(uint[] storage arr) internal {
    arr.push(123);
}



控制结构

JavaScript 中的大部分控制结构在 Solidity 中都是可用的,除了

switch 和 goto



因此 Solidity 中有

if, else, while, do, for, break, continue, return, ? :



Solidity还支持

try/ catch

语句形式的异常处理,但仅用于 外部函数调用 和合约创建调用。

使用:ref:revert 语句 可以触发一个”错误”。

用于表示条件的

括号

不可以 被省略,单语句体 两边的 花括号 可以被省略。


注意

:与 C 和 JavaScript 不同, Solidity 中 非布尔类型数值 不能转换为 布尔类型,因此

if (1) { ... }

的写法在 Solidity 中 无效 。

pragma solidity >=0.4.22 < 0.8.0;

contract MyCounter {
    uint256 counter;

    constructor() public {
        counter = 1;
    }
    
    function getCounter() public view returns (uint256) {
        return counter;
    }

    function IterAdd(uint amount) public {
        for (uint i=0; i<amount; i++) {
            if (i % 2 == 0) {
                counter +=i;
            }
        }
    }

    function LetZero() public {
        counter = 0;
    }
}



合约

Solidity 中的 合约 类似于 面向对象语言中的类。

合约中有用于数据持久化的 状态变量,和可以修改 状态变量 的 函数。

调用另一个合约实例的函数时,会执行一个 EVM 函数调用,这个操作会切换执行时的上下文,这样,前一个合约的状态变量就不能访问了。



创建合约



方式

  1. UI用户界面使创建合约

一些集成开发环境,例如

Remix

, 使得 创建合约 的过程更加顺畅。

  1. 通过编程创建合约

在 以太坊 上通过 编程创建合约 最好使用

JavaScript API web3.js



构造器

合约的 构造函数(构造器) 是一个用关键字

constructor

声明的 匿名函数。


创建合约时

,构造器会自动执行一次。 构造函数是可选的。

一个合约只允许有一个构造函数,这意味着不支持重载。

pragma solidity >=0.4.22 < 0.8.0;

contract MyCounter {
    uint256 counter;
    // 构造器
    constructor () public {
        counter = 1;
    }
}

构造函数 执行完毕后,合约的 最终代码 将 部署到区块链上。

此代码包括:所有公共外部函数、所有可以通过函数调用访问的函数。

部署的代码不包括:构造函数代码、构造函数调用的内部函数。



可见性 和 getter函数



可见性 标识符 的 定义位置

  • 状态变量

    在 类型 后面
  • 函数

    在 参数列表 和 返回关键字 中间
contract C {
    uint public data;
    function setData(uint a) internal { data = a; }
}



状态变量 的 可见性

  • public

对于 public 状态变量 会

自动生成

一个 getter函数,以便其他的合约读取他们的值。

当被另一个合约中使用时,外部方式访问 (如: this.x) 会调用getter 函数,而内部方式访问 (如: x) 会直接从存储中获取值。

Setter函数则不会被生成,所以其他合约不能直接修改其值。

  • internal

内部可见性 状态变量 只能在它们 所定义的合约 和 派生合约(继承子类)中访问。

它们不能被外部访问。 这是 状态变量的

默认可见性

  • private

私有 状态变量 在 派生合约 中是不可见的。



函数(方法) 的 可见性

由于 Solidity 有两种函数调用:1.外部调用 (会产生一个 EVM); 2.内部调用。

这里有 4 种可见性:

  • external


外部可见性函数

作为 合约接口 的一部分,意味着我们可以从

其他合约



交易

中调用。

一个外部函数 f 不能从 内部调用(即 f 不起作用,但 this.f() 可以)。

  • public


public 函数

是 合约接口 的一部分,可以在

内部



通过消息

调用。

  • internal


内部可见性函数

访问 可以在

当前合约



派生的合约

访问,

不可以外部访问。

由于它们没有通过合约的ABI向外部公开,它们可以接受内部可见性类型的参数:比如映射或存储引用。

  • private


private 函数

仅在

当前定义它们的合约

中使用,并且不能被派生合约使用。

contract C {
    uint private data;

    // public
    function publicTest() public returns(uint) {
        return data; 
    }

    // internal
    function internalTest(uint a, uint b) internal returns (uint) {
        return a+b; 
    }

    // private
    function f(uint a) privateTest returns(uint b) {
        return a + 1; 
    }
}

contract D {
    function readData() public {
        C c = new C();
        uint local = c.publicTest(); // 可以
        local = c.internalTest();    // 不可以
        local = c.privateTest(3, 5); // 不可以
    }
}

contract E is C {
    function g() public {
        C c = new C();
        uint local = c.publicTest(); // 可以
        local = c.internalTest();    // 可以
        local = c.privateTest(3, 5); // 不可以
    }
}



Getter 函数

编译器 会为所有

public 状态变量

自动创建 getter 函数。

getter 函数具有外部可见性。

如果你有一个数组类型的 public 状态变量,

那么你只能通过生成的 getter 函数访问数组的单个元素。

这个机制以避免返回整个数组时的高成本gas。

contract Wtt {
  uint[] public arr;

  // 返回单个元素
  /* 自动生成 的 Getter 函数 和 状态变量 同名 
     其 逻辑如下,也可以 手动指定生成的Getter 函数,使得完成更复杂的功能。
  function arr(uint i) public view returns (uint) {
      return arr[i];
  }
  */

  // 返回整个数组
  function getAllArr() public view returns (uint[] memory) {
      return arr;
  }
}



Constant 和 Immutable 状态变量

状态变量 声明为

constant (常量)

或者

immutable (不可变量)

时,

合约一旦部署之后,变量 将不再修改。

对于 constant 常量, 他的值在

编译时

确定,而对于 immutable, 它的值在

部署时

确定。

常量 和 不可变量 的 gas成本 要低得多。

不是 所有类型 的 状态变量 都支持用 constant 或 immutable 来修饰,

当前 仅支持 字符串 (仅常量) 和 值类型.

contract C {
    string constant aaa = "abc";
    uint immutable bbb;
}



函数

可以在合约 内部 和 外部 定义函数。外部定义 是从 0.7.0 之后才开始支持的。

合约之外的函数(也称为“

自由函数

”)始终具有隐式的

internal 可见性



它们的代码包含在 所有调用它们合约中,类似于 内部库函数。

pragma solidity >=0.7.1 <0.8.13;

// 定义 自由函数
function sum(uint[] memory arr) pure returns (uint s) {
    for (uint i = 0; i < arr.length; i++)
        s += arr[i];
}

contract ArrayExample {
    function f(uint[] memory arr) public {
        // 调用 自由函数
        uint s = sum(arr);
    }
}



状态可变性

  • View 视图函数

可以将函数声明为 view 类型,这种情况下要保证

不修改 状态

contract C {
    function f(uint a, uint b) public view returns (uint) {
        return a * (b + 42) + block.timestamp;
    }
}
  • Pure 纯函数

函数可以声明为 pure ,这种情况下要保证

不读取 也 不修改 状态变量

contract C {
    function f(uint a, uint b) public pure returns (uint) {
        return a * (b + 42);
    }
}



特别的函数

  • receive 接收以太函数一个合约最多有一个 receive 函数,

不需要 function 关键字,也没有 参数 和 返回值,

必须是 external 可见性和 payable 修饰.

在对 合约

没有任何附加数据

的调用(通常是

对合约转账

)会执行 receive 函数.

pragma solidity ^0.6.0;

// 这个合约会 保留 所有发送给它的以太币,但是没有办法取回。
contract Sink {
    event Received(address, uint);
    receive() external payable {
        emit Received(msg.sender, msg.value);
    }
}
  • Fallback 回退函数

合约可以最多有一个回退函数。没有function关键字。 必须是external可见性.

在合约调用中,

没有其他函数 与 给定的函数标识符 匹配

则fallback会被调用;



没有 receive 函数

时,也

没有提供附加数据对合约调用

,则fallback会被执行。

fallback函数 始终会 接收数据,但为了 能同时接收以太 ,必须标记为 payable 。

// 发送 到这个 合约 的 所有消息 都会调用fallback函数(因为该合约没有其它函数)。
// 向这个 合约 发送 以太币 会导致异常,因为 fallback 函数没有 `payable` 修饰符
contract Test {
    fallback() external { x = 1; }
    uint x;
}



函数重载

合约 可以具有 多个 不同参数 的 同名函数,称为“重载”(overloading),

这也适用于继承函数。以下示例展示了合约 A 中的重载函数 f。

contract A {
    function aaa(uint value) public pure returns (uint out) {
        out = value;
    }

    function aaa(uint value, bool really) public pure returns (uint out) {
        if (really)
            out = value;
    }
}



事件 Events

Solidity 的 事件 是EVM的

日志功能 之上的 抽象



是以 太坊虚拟机(EVM)日志 基础设施 提供的一个 便利接口。

应用程序 可以通过 以太坊客户端 的 RPC接口 订阅 和 监听 这些事件。

事件在合约中可被继承。

当他们被调用时,会使

参数

被存储到

交易的日志

中,

交易日志 是一种区块链中的 特殊数据结构。

这些 日志 与 地址 相关联,被并入 区块链 中,只要区块可以访问就一直存在。

日志 和 事件 在合约内 不可直接被访问(甚至是 创建日志 的 合约 也不能访问)。

来捋这个关系:区块链 是打包一系列 交易的区块组成的链条,

每一个 交易“收据” 会包含0到多个日志记录,

日志 中记录着 该智能合约 所触发的事件。



定义时间

使用

event

关键字来定义一个事件,如:

event EventName(address bidder, uint amount); 



出发事件

触发事件可以在任何函数中调用,如:

function testEvent() public {

    // 触发一个事件
     emit EventName(msg.sender, msg.value); 
}



监听事件

通过上面的介绍,可能大家还是不清楚事件有什么作用。

下面这个合约,在前端点击”Updata Info”按钮之后,虽然调用智能合约成功,但是当前的界面并没有得到更新。

使用事件监听,就可以很好的解决 数据更新的 问题,让看看如何实现。

pragma solidity ^0.4.21;

contract InfoContract {
   string fName;
   uint age;
   
   function setInfo(string _fName, uint _age) public {
       fName = _fName;
       age = _age;
   }
   
   function getInfo() public constant returns (string, uint) {
       return (fName, age);
   }   
}

首先,需要定义一个事件

然后,需要在setInfo函数中,触发Instructor事件,如:

pragma solidity ^0.4.21;

contract InfoContract {
    // 这个事件中,会接受两个参数:name 和 age , 也就是 需要跟踪 的 两个信息。
    event Instructor(
        string name,
        uint age
    );
    
   string fName;
   uint age;
   
   function setInfo(string _fName, uint _age) public {
       fName = _fName;
       age = _age;
       // 出发时间
       emit Instructor(_fName, _age);
   }
   
   function getInfo() public constant returns (string, uint) {
       return (fName, age);
   }   
}

使用 JavaScript API 调用事件的用法如下:

<script>
    if (typeof web3 !== 'undefined') {
        web3 = new Web3(web3.currentProvider);
    } else {
        // set the provider you want from Web3.providers
        web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:7545"));
    }

    web3.eth.defaultAccount = web3.eth.accounts[0];

    var infoContract = web3.eth.contract(ABI INFO);

    var info = infoContract.at('CONTRACT ADDRESS');

    // 获取信息
    info.getInfo(function(error, result){
        if(!error)
            {
                $("#info").html(result[0]+' ('+result[1]+' years old)');
                console.log(result);
            }
        else
            console.error(error);
    });

    $("#button").click(function() {
        info.setInfo($("#name").val(), $("#age").val());
    });

</script>

现在可以不需要 info.getInfo()来获取信息,而改用监听事件获取信息,先定义一个变量引用事件,

然后使用.watch()方法来添加一个回调函数:

<script>
    if (typeof web3 !== 'undefined') {
        web3 = new Web3(web3.currentProvider);
    } else {
        // set the provider you want from Web3.providers
        web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:7545"));
    }

    web3.eth.defaultAccount = web3.eth.accounts[0];

    var infoContract = web3.eth.contract(ABI INFO);

    var info = infoContract.at('CONTRACT ADDRESS');

    // 改为:
    var instructorEvent = info.Instructor();
    instructorEvent.watch(function(error, result) {
        if (!error)
            {
                $("#info").html(result.args.name + ' (' + result.args.age + ' years old)');
            } else {
                console.log(error);
            }
    });

    $("#button").click(function() {
        info.setInfo($("#name").val(), $("#age").val());
    });

</script>

代码更新之后,可以在浏览器查看效果,这是点击”Updata Info”按钮之后,会及时更新界面.



错误 和 回退语句

错误(关键字error)提供了 一种方便 且 省gas 的方式

来向 用户 解释 一个操作 为什么会失败。

它们可以被定义在合约(包括接口和库)内部和外部。

错误必须与 revert 语句 一起使用。它会还原当前调用中的发生的所有变化,并将错误数据传回给调用者。

pragma solidity ^0.8.4;

/// 转账时,没有足够的余额。
/// @param available balance available.
/// @param required requested amount to transfer.
error InsufficientBalance(uint256 available, uint256 required);

contract TestToken {
    function transfer(address to, uint256 amount) public {
        if (amount > balance[msg.sender])
            revert InsufficientBalance({
                available: balance[msg.sender],
                required: amount
            });
    }
}

错误不能被重写或覆盖,但是可以继承。

只要作用域不同,同一个错误可以在多个地方定义。

只能使用 revert 语句 创建 错误实例。



继承

Solidity 支持

多重继承

包括多态。

当一个合约从多个合约继承时,在区块链上只有 一个合约被创建 即 子合约,

所有基类合约(或称为父合约)的代码 都被编译到 子合约中。

总的来说,Solidity 的继承系统与 Python的继承系统 非常 相似,特别是多重继承方面, 但是也有一些 不同。

pragma solidity >=0.4.22 < 0.8.0;

contract aaa {
    uint256 counter;
 
    constructor () public {
        counter = 1;
    }
    
    // 关键字`virtual`表示该函数可以在派生类中 被 重写。
    function getCounter() virtual public view returns (uint256) {
        return counter;
    }

}

// 抽象类(抽象合约)
abstract contract bbb {
    function testBbb(uint id) public virtual returns (uint id);
}

// 使用 is 继承合约 aaa
contract ccc is aaa, bbb {
    function testBbb(uint id) public virtual returns (uint id){
        return id;
    }
}



接口

接口类似于抽象合约,但是它们不能实现任何函数。还有进一步的限制:

  • 无法继承其他合约,不过可以继承其他接口。
  • 接口中所有的函数都需要是 external,尽管在合约里可以是public
  • 无法定义构造函数。
  • 无法定义状态变量。
  • 不可以声明修改器。
pragma solidity >=0.6.2 <0.8.0;

interface Token {
    enum TokenType { Fungible, NonFungible }
    struct Coin { string obverse; string reverse; }
    function transfer(address recipient, uint amount) external;
}



特殊变量 和 函数

在 全局命名空间 中 已经存在了(预设了)一些 特殊的 变量 和 函数,

他们主要用来 提供关于 区块链 的 信息 或一些 通用的 工具函数。



区块和交易属性

  • blockhash(uint blockNumber) returns (bytes32):指定区块的区块哈希 —— 仅可用于最新的 256 个区块且不包括当前区块,否则返回 0 。
  • block.basefee (uint): 当前区块的基础费用,参考: (EIP-3198 和 EIP-1559)
  • block.chainid (uint): 当前链 id
  • block.coinbase ( address ): 挖出当前区块的矿工地址
  • block.difficulty ( uint ): 当前区块难度
  • block.gaslimit ( uint ): 当前区块 gas 限额
  • block.number ( uint ): 当前区块号
  • block.timestamp ( uint): 自 unix epoch 起始当前区块以秒计的时间戳
  • gasleft() returns (uint256) :剩余的 gas
  • msg.data ( bytes ): 完整的 calldata
  • msg.sender ( address ): 消息发送者(当前调用)
  • msg.sig ( bytes4 ): calldata 的前 4 字节(也就是函数标识符)
  • msg.value ( uint ): 随消息发送的 wei 的数量
  • tx.gasprice (uint): 交易的 gas 价格
  • tx.origin ( address ): 交易发起者(完全的调用链)
pragma solidity >=0.4.22 < 0.8.0;

contract MyCounter {
    function consolelog() public view returns (address aaa,uint bbb, address ccc) {
        aaa = msg.sender; 
        bbb = block.number;
        ccc = tx.origin;
    }
}



ABI

如果理解 API 就很容易了解 ABI。简单来说,API 是 程序 与 程序 间互动的接口。

这个接口 包含 程序提供 外界存取所需的 functions、variables 等。

ABI 也是 程序间 互动的接口,但程序是

被编译后

的 binary code。

所以同样的接口,但传递的是 binary 格式的信息。

所以 ABI 就要描述:

如何 decode/encode 程序间传递的 binary 信息



编译和部署

从 智能合约的代码 到 使用智能合约,大概包含

编写、编译、部署、调用

这几个步骤:

  1. 编写智能合约的代码(一般是用 Solidity 写)

  2. 编译智能合约的代码变成可在 EVM 上执行的 bytecode(binary code)。同时可以通过编译取得智能合约的 ABI

  3. 部署智能合约,实际上是把 bytecode 存储在链上(通过一个transaction),并取得一个专属于这个合约的地址

  4. 如果要写个程序调用这个智能合约,就要把信息发送到这个合约的地址(一样的也是通过一个 transaction)。Ethereum 节点会根据输入的信息,选择要执行合约中的哪一个 function 和要输入的参数

而要

如何知道這这个智能合约提供哪些 function 以及应该要传入什么样的参数呢?

这些信息就是

记录在智能合约的 ABI



Ethereum 智能合约 ABI

ABI 用一个 数组 表示,其中会包含 数个用 JSON 格式表示的 Function 或 Event。



Function 共有 7 个参数

  • name:a string,function 名称
  • type:a string,“function”, “constructor”, or “fallback”
  • inputs:an array,function 输入的参数,包含:
  • name:a string,参数名
  • type:a string,参数的 data type(e.g. uint256)
  • components:an array,如果输入的参数是 tuple(struct) type 才会有这个参数。描述 struct 中包含的参数类型
  • outputs:an array,function 的返回值,和 inputs 使用相同表示方式。如果沒有返回值可忽略,值为 []
  • payable:true,function 是否可收 Ether,预设为 false
  • constant:true,function 是否会改写区块链状态,反之为 false
  • stateMutability:a string,其值可能为以下其中之一:“pure”(不会读写区块链状态)、“view”(只读不写区块链状态)、“payable” and “nonpayable”(会改区块链状态,且如可收 Ether 为 “payable”,反之为 “nonpayable”)



Event 共有 4 个参数

  • name: a string,event 的名称
  • type: a string,always “event”
  • inputs: an array,输入参数,包含:
  • name: a string,参数名称
  • type: a string,参数的 data type(e.g. uint256)
  • components: an array,如果输入参数是 tuple(struct) type 才会有这个参数。描述 struct 中包含的信息类型
  • indexed: true,如果这个参数被定义为 indexed ,反之为 false
  • anonymous: true,如果 event 被定义为 anonymous


例子:

/*
这个智能合约包含:
    data:一个可修改的 状态变量,会自动产生一个只能读取的 data() function
    set():一个修改 data 值的 function
    Set():一个在每次修写 data 时记录 Log 的 event
*/

pragma solidity ^0.4.20;
contract SimpleStorage {
    uint public data;
    event Set(address indexed _from, uint value);
    function set(uint x) public {
        data = x;
        Set(msg.sender, x);
    }
}

智能合约 ABI:

[
    {
        "constant": true,
        "inputs": [],
        "name": "data",
        "outputs": [{"name": "","type": "uint256"}],
        "payable": false,
        "stateMutabㄒility": "view",
        "type": "function"
    },
    {
        "anonymous": false,
        "inputs": [{"indexed": true,"name": "_from","type": "address"},{"indexed": false,"name": "value","type": "uint256"}],
        "name": "Set",
        "type": "event"
    },
    {
        "constant": false,
        "inputs": [{"name": "x","type": "uint256"}],
        "name": "set",
        "outputs": [],
        "payable": false,
        "stateMutability": "nonpayable",
        "type": "function"
    }
]



获取 abi



命令形式

npm install solc -g

solcjs aaa.sol --abi # 此时 在当前目录 就会 生成 相应的 abi文件。

# 也可以取得合约的 binary code:
solcjs aaa.sol --bin



GUI形式

同样的使用 Solidity Compiler,也可以用 Remix。

在合约 编译界面 的

Details

可以看到完整的 ABI。

也可以在 Settings 中指定 Compiler 版本。



项目实例



众筹项目

pragma solidity ^0.5.8;

contract zhongchouTest {
    // 捐赠者
    struct funder {
        address funderAddress; // 捐赠者地址
        uint toMoney; // 捐赠money
    }
    // 受益人对象
    struct needer {
        address payable neederAddress; // 受益人地址
        uint goal; // 众凑总数(目标)
        uint amount; // 当前募资金额
        uint isFinish; // 募资是否完成
        uint funderAccount; // 当前捐赠者人数
        mapping(uint => funder) funderMap; // 映射,将捐赠者的id与捐赠者绑定在一起,从而得知是谁给受益人捐钱
    }
    
    uint Id; // 众筹项目id,由于 多个人需要帮助 故会发起多个 众筹项目,因此用 id进行 项目标识
    mapping (uint => needer) neederMap; // 通过mapping将受益人id与收益金额绑定在一起,从而可以更好的管理受益人
    
    // 新建众筹项目
    /*
    * _neederAddress: 受益人地址(项目发起者)
    * _goal: 众筹目标
    */
    function NewNeeder(address payable _neederAddress, uint _goal) public {
        Id++; // 众筹项目id 从 1 开始
        // 记录 项目id 和 受益人 的联系
        neederMap[Id] = needer(address(_neederAddress), _goal, 0, 0, 0);
    }
    
    // 捐赠者给指定众筹id 打钱
    /*
    *_address: 捐赠者地址
    *_id: 众筹项目id
    */
    function contribue(address _address, uint _id) public payable {
        require(msg.value > 0);
        needer storage _needer = neederMap[_id]; // 通过 众筹项目id 获取 指定的 受益人对象
        require(_needer.isFinish == 0); // 募资是否完成, 若完成则取消当前捐款

        _needer.amount += msg.value; // 统计 到合约账户的 捐赠金额
        _needer.funderAccount++; // 捐赠者个数
        _needer.funderMap[_needer.funderAccount] = funder(_address, msg.value); // 记录 捐赠者 及 捐赠金额
    }
    
    // 捐赠是否完成,若完整,给 受益人 转账
     /*
    *_id: 众筹项目id
    */
    function Iscompelete(uint _id) public payable {
        needer storage _needer = neederMap[_id]; // 获取众筹项目
        require(_needer.amount >= _needer.goal);

        // transfer 向指定 地址 发送 数量为 amount的 ether(单位wei)
        _needer.neederAddress.transfer(_needer.amount);// 合约账户 ===转账到===> 个人账户

        _needer.isFinish = 1; // 若完成募资,则取消继续募资
    }
    
    //  募资完成时,退款给捐赠人
    function returnBack(uint _id) public payable {
        needer storage _needer = neederMap[_id]; // 获取众筹项目
        require(_needer.funderMap[_needer.funderAccount].funderAddress == msg.sender);
        uint returnMoney = _needer.funderMap[_needer.funderAccount].toMoney;
         
        uint balance = address(this).balance;
         
        balance -= returnMoney;

        // msg.sender 合约调用者 地址
        // transfer 向指定 地址 发送 数量为 amount的 ether(单位wei)
        msg.sender.transfer(returnMoney);
    }
    
    // 查询合约余额
    function getBalance() public view returns(uint,uint) {
        // balance 查询该地址的 ether余额
        return (address(this).balance, Id);
    }
    
    // 查看募资状态, 以 具名返回值 的形式 更方便查看 数据
    function showData(uint _id) public view returns(uint goalNum, uint hasGetNum, uint funderNum, uint isFinish) {
        goalNum = neederMap[_id].goal;
        hasGetNum = neederMap[_id].amount;
        funderNum = neederMap[_id].funderAccount;
        isFinish = neederMap[_id].isFinish;
    }
}



版权声明:本文为weixin_45541665原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。