您现在的位置是:首页 >技术交流 >Solidity基础八网站首页技术交流
Solidity基础八
别慌,月亮也在大海某处迷茫
目录
一、Solidity 编程风格
良好统一的编程风格,有助于提高代码的可读性和可维护性。
下面是有关Solidity编程风格的几条建议。
1. 代码布局
- 缩进
使用4个空格代替制表符作为缩进,避免空格与制表符混用。- 空2行规则
2个合约定义之间空2行。pragma solidity ^0.5.0; contract LedgerBalance { //... } contract Updater { //... }
- 空1行规则
2个函数之间空1行。在只有声明的情况下,不需要空行。pragma solidity ^0.5.0; contract A { function balance() public pure; function account() public pure; } contract B is A { function balance() public pure { // ... } function account() public pure { // ... } }
- 行长度
一行不超过79个字符。- 换行规则
函数声明中左括号不换行,每个参数一行并缩进,右括号换行,并对齐左括号所在行。function_with_a_long_name( longArgument1, longArgument2, longArgument3 ); variable = function_with_a_long_name( longArgument1, longArgument2, longArgument3 ); event multipleArguments( address sender, address recipient, uint256 publicKey, uint256 amount, bytes32[] options ); MultipleArguments( sender, recipient, publicKey, amount, options );
- 源码编码
UTF-8- Import
Import语句应该放在文件的顶部,pragma声明之后。- 函数顺序
函数应该根据它们的可见性来分组。pragma solidity ^0.5.0; contract A { constructor() public { // ... } function() external { // ... } // External functions // ... // External view functions // ... // External pure functions // ... // Public functions // ... // Internal functions // ... // Private functions // ... }
- 避免多余空格
避免在圆括号、方括号或大括号后有空格。- 控制结构
大括号的左括号不换行,右括号换行,与左括号所在行对齐。pragma solidity ^0.5.0; contract Coin { struct Bank { address owner; uint balance; } } if (x < 3) { x += 1; } else if (x > 7) { x -= 1; } else { x = 5; } if (x < 3) x += 1; else x -= 1;
- 函数声明
使用上面的大括号规则。添加可见性标签。可见性标签应该放在自定义修饰符之前。function kill() public onlyowner { selfdestruct(owner); }
- 映射
在声明映射变量时避免多余空格。mapping(uint => uint) map; // 不是 mapping (uint => uint) map; mapping(address => bool) registeredAddresses; mapping(uint => mapping(bool => Data[])) public data; mapping(uint => mapping(uint => s)) data;
- 变量声明
声明数组变量时避免多余空格。uint[] x; // 不是 unit [] x;
- 字符串声明
使用双引号声明字符串,而不是单引号。str = "foo"; str = "Hamlet says, 'To be or not to be...'";
2. 代码中各部分的顺序
代码中各部分顺序如下:
- Pragma 语句
- Import 语句
- Interface
- 库
- Contract
在Interface、Library或Contract中,各部分顺序应为:
- Type declaration / 类型声明(enum,struct)
- State variable / 状态变量
- Event / 事件
- Function / 函数
3. 命名约定
- 合约和库应该使用驼峰式命名。例如,SmartContract, Owner等。
- 合约和库名应该匹配它们的文件名。
- 如果文件中有多个合约/库,请使用核心合约/库的名称。
owned.sol
pragma solidity ^0.8.0; // Owned.sol contract Owned { address public owner; constructor() public { owner = msg.sender; } modifier onlyOwner { //.... } function transferOwnership(address newOwner) public onlyOwner { //... } }Congress.sol
pragma solidity ^0.8.0; // Congress.sol import "./Owned.sol"; contract Congress is Owned, TokenRecipient { //... }
- 结构体名称
驼峰式命名,例如: SmartCoin- 事件名称
驼峰式命名,例如:AfterTransfer- 函数名
驼峰式命名,首字母小写,比如:initiateSupply- 局部变量和状态变量
驼峰式命名,首字母小写,比如creatorAddress、supply- 常量
大写字母单词用下划线分隔,例如:MAX_BLOCKS- 修饰符的名字
驼峰式命名,首字母小写,例如:onlyAfter- 枚举的名字
驼峰式命名,例如:TokenGroup
二、Solidity 智能合约编写过程
要写智能合约有好几种语言可选:有点类似 Javascript 的 Solidity, 文件扩展名是.sol。Python 接近的Serpent, 文件名以.se结尾。还有类似 Lisp 的LLL,但现在最流行而且最稳定的要算是 Solidity 了。
1. solidity Hello World
pragma solidity ^0.4.0; import "./A.sol"; contract HelloWorld { function hello() returns(string){ return "hello world"; } }solidity文件扩展名为.sol,主合约名要和solidity文件名相同,一份合约包含版本声明,导入声明,合约声明
2. 版本声明
pragma solidity ^0.4.0;pragmas(编译指令)是告知编译器如何处理源代码的指令,^表示向上兼容,版本操作符可以为:^ ~ >= > < <= = 之一,0.4.0代表solidity版本(版本字面量形如x.x.x),^0.4.0表示solidity的版本在0.4.0 ~ 0.5.0(不包含0.5.0)的版本,这是为了确保合约不会在新的编译器版本中突然行为异常
3. 导入声明
import导入其他源文件,例如:
import "./A.sol"; 从"A"中导入所有的全局标志到当前全局范围,`./`表示当前目录其他方式:
(1). 创建新的全局符号 symbolName,其成员都是来自“A”的全局。
import * as symbolName from "A";/import "A" as symbolName;(2).创建新的全局符号“alias”和“symbol2”,它将分别从”A” 引入symbol1 和 symbol2。
import {symbol1 as alias, symbol2} from "A";
4. 合约声明
包括:contract,interface,library。
(1).contract
contract HelloWorld这里HelloWorld指合约名称,contract即为其他语言中的class,HelloWorld即为类名,创建合约实例则用HelloWorld helloworld = new HelloWorld();
(2).interface 接口
interface A{ function testA(); }函数不允许有函数体。
(3).library 库
库与合约类似,但它的目的是在一个指定的地址,且仅部署一次,然后通过 EVM 的特性来复用代码。
library Set { struct Data { mapping(uint => bool) flags; } function test(){ } }其他合约调用库文件的内容直接通过库文件名.方法名例如:Set.test()。
注:一份源文件可以包含多个版本声明、多个导入声明和多个合约声明。
三、Solidity 合约结构
本章节主要讲述智能合约中合约的基本结构,及基本关键字的使用。
合约中可包含内容:
usingFor声明,状态变量(State Variables),结构类型(Structs Types),构造函数,函数修饰符(Function Modifiers),函数(Functions),事件(Events),枚举类型(Enum Types)
1.智能合约 Test
pragma solidity ^0.4.0; //版本声明
import "./A.sol"; //导入声明
contract SolidityStructure{ //合约声明
uint balance;//状态变量
address owner;
struct Hello { // 结构类型
uint helloNum;
address hello;
constructor() public{ //构造函数
owner = msg.sender;
}
//function HelloWorld(){
//} 这种方式也可以
modifier onlySeller() { // 修饰器
require(
msg.sender != owner
);
_;
}
function test() public { //函数
uint step = 10;
if (owner == msg.sender) {
balance = balance + step;
}
}
function update(uint amount) constant returns (address, uint){ //带返回值的函数
balance += amount;
return (msg.sender, balance);
}
using LibraryTest for uint; //using声明
uint a = 1;
function getNewA()returns (uint){
return a.add();
}
function kill() { //析构函数
if (owner == msg.sender) {
selfdestruct(owner);
}
}
event HighestBidIncreased(address bidder, uint amount);//事件 log日志打印
function bid() public payable {
emit HighestBidIncreased(msg.sender, msg.value); // 触发事件打印相关日志
}
enum State { Created, Locked, Inactive } // 枚举
}
状态变量
uint balance;类似java中类的属性变量,状态变量是永久的存储在合约中的值(强制是storage类型)
状态变量可以被定义为constant即常量,例如:uint constant x = 1;结构类型
struct Hello { // 结构类型 uint helloNum; address hello; }自定义的将几个变量组合在一起形成的类型,有点类似javabean
构造函数
constructor() public{ owner = msg.sender; }构造函数可用constructor关键字进行声明,也可用function HelloWorld(){} 这种方式声明,当合约对象创建时会先调用构造函数对数据进行初始化操作,构造函数只允许存在一个
函数修饰符(函数修改器)
modifier onlySeller() { // 修饰器 require( msg.sender != owner ); _; }函数修饰符用于’增强语义’,可以用来轻易的改变一个函数的行为,比如用于在函数执行前检查某种前置条件。修改器是一种合约属性,可被继承,同时还可被派生的合约重写。_表示使用修改符的函数体的替换位置。当然函数修饰器可以传参数
成员函数
function test() public / function update(uint amount) constant returns (address,uint)这两种都可以为合约的成员函数,成员函数类似java中基本函数,但是略有不同,不同点在于有返回值时在函数上指定返回值returns(uint),函数调用方式可以设置为内部(Internal)的和外部(External)的,在权限章节会进行介绍
注意:constant只是一个承诺,承诺该函数不会修改区块链上的状态
###using for
使用方式是using A for B
用来把A中的函数关联到到任意类型B,B类型的对象调用A里面的函数,被调用的函数,将会默认接收B类型的对象的实例作为第一个参数。pragma solidity ^0.4.0; library LibraryTest{ function use(uint a) returns(uint){ return a+1; } } contract usingTest{ using LibraryTest for uint;//把LibraryTest中的函数关联到uint类型 uint test = 1; function testusing() returns (uint){ return test.use();//uint类型的对象实例test调用LibraryTest里的函数add();add()会默认接收test作为第一个参数。 } }析构函数
selfdestruct()所谓的析构函数是和构造函数相对应,构造函数是初始化数据,而析构函数是销毁数据
事件
event HighestBidIncreased(address bidder, uint amount);//事件 log日志打印 function bid() public payable { emit HighestBidIncreased(msg.sender, msg.value); // 触发事件打印相关日志 }事件是以太坊虚拟机(EVM)日志基础设施提供的一个便利接口。用于获取当前发生的事件。事件在合约中可被继承。
枚举
enum State { Created, Locked, Inactive } // 枚举他可以显式的转换与整数进行转换,但不能进行隐式转换。显示的转换会在运行时检查数值范围,如果不匹配,将会引起异常。 枚举类型应至少有一名成员。
四、Solidity 常见编译错误
1、报错:Expected token Semicolon got 'eth_compileSolidity' funtion setFunder(uint _u,uint _amount){
解决:funtion关键字错了,需要用function;
2、报错:Variable is declared as a storage pointer. Use an explicit "storage" keyword to silence this warning. Funder f = funders[_u]; ^------^
解决:Funder f,定义指针需要加关键字storage ;修改为Funder storage f = funders[_u];
3、报错:Invoking events without "emit" prefix is deprecated. e("newFunder",_add,_amount); ^-------------------------^
解决:调用事件需要在前面加上emit关键字,修改为emit e("newFunder",_add,_amount);
4、报错:No visibility specified. Defaulting to "public". function newFunder(address _add,uint _amount) returns (uint){ ^ (Relevant source part starts here and spans across multiple lines).
解决:定义函数必须加上public关键字,修改为function newFunder(address _add,uint _amount) public returns (uint){
5、报错:"msg.gas" has been deprecated in favor of "gasleft()" uint public _gas = msg.gas; ^-----^
解决:msg.gas已经被gasleft()替换了。修改为uint public _gas = gasleft();
6、报错:"throw" is deprecated in favour of "revert()", "require()" and "assert()". throw ;
解决:solidity已经不支持thorw了,需要使用require,用法require()
throw 写法:
if(msg.sender !=chairperson ||voters[_voter].voted ){
throw ;
}
require写法:
require(msg.sender !=chairperson ||voters[_voter].voted);
7、报错:This declaration shadows an existing declaration. Voter delegate = voters[to]; ^------------^
解决:变量重复定义,变量名和函数名不能相同。
8、报错:error: Function state mutability can be restricted to pure
解决:以前版本是可以不指定类型internal pure(外部不可调用),public pure(外部可调用)(如不指定表示函数为可变行,需要限制)
9、报错:"sha3" has been deprecated in favour of "keccak256"
解决:sha3已经替换为keccak256
五、Solidity 调用合约
Solidity 支持一个合约调用另一个合约。两个合约既可以位于同一sol文件,也可以位于不同的两个sol文件。
Solidity 还能调用已经上链的其它合约。
1.调用内部合约
内部合约是指位于同一sol文件中的合约,它们不需要额外的声明就可以直接调用。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Hello { function echo() external pure returns(string memory){ return "Hello World!"; } } contract SoldityTest { function callHello(address addr) external pure returns(string memory){ // 调用外部合约 Hello 的方法 echo return Hello(addr).echo(); } // 另外一种写法 function callHelloOr(Hello hello) external pure returns(string memory){ // 调用外部合约 Hello 的方法 echo return hello.echo(); } }我们在部署上面两个合约的时候,首先要部署 Hello,得到它的地址,例如:0x78FD83768c7492aE537924c9658BE3D29D8ffFc1。
然后再部署合约 SoldityTest,调用 SoldityTest 的方法 callHello,传入参数 0x78FD83768c7492aE537924c9658BE3D29D8ffFc1 ,就会输出调用结果:"Hello World!"。
2.调用外部合约
外部合约是指位于不同文件的外部合约,以及上链的合约。
调用外部合约有两种方法:通过接口方式调用 和 通过签名方式调用。
通过接口方式调用
通过接口方式调用合约,需要在调用者所在的文件中声明被调用者的接口。
被调用者合约 hello.sol:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract IHello { function echo() external pure returns(string memory){ return "Hello World!"; } }调用者合约 test.sol:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; // 被调用者接口 interface IHello { // 被调用的方法 function echo() external pure returns(string memory); } // 调用者合约 contract SoldityTest { function callHello(address addr) external pure returns(string memory){ // 调用外部合约Hello的方法:echo return IHello(addr).echo(); } }我们首先要部署 hello.sol 文件中的合约 Hello,得到它的地址,例如:0x78FD83768c7492aE537924c9658BE3D29D8ffFc1。
然后再部署合约 SoldityTest,调用 SoldityTest 的方法 callHello,传入参数 0x78FD83768c7492aE537924c9658BE3D29D8ffFc1 ,就会输出调用结果:"Hello World!"。
通过签名方式调用
通过签名方式调用合约,只需要传入被调用者的地址和调用方法声明。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; // 调用者合约 contract SoldityTest { function callHello(address addr) external returns(string memory){ // 调用合约 (bool success,bytes memory data) = addr.call(abi.encodeWithSignature("echo()")); if(success){ return abi.decode(data,(string)); } else { return "error"; } } }另一种写法:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; // 调用者合约 contract SoldityTest { function callHello(address addr) external view returns(string memory){ // 编码被调用者的方法签名 bytes4 methodId = bytes4(keccak256("echo()")); // 调用合约 (bool success,bytes memory data) = addr.staticcall(abi.encodeWithSelector(methodId)); if(success){ return abi.decode(data,(string)); } else { return "error"; } } }我们首先要部署 hello.sol 文件中的合约 Hello,得到它的地址,例如:0x78FD83768c7492aE537924c9658BE3D29D8ffFc1。
然后再部署合约 SoldityTest,调用 SoldityTest 的方法 callHello,传入参数 0x78FD83768c7492aE537924c9658BE3D29D8ffFc1 ,就会输出调用结果:"Hello World!"。
签名方式调用,发送Eth
通过签名方式调用合约,可以发送Eth。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; // 调用者合约 contract SoldityTest { function callHello(address addr) external returns(string memory){ // 调用合约 (bool success,bytes memory data) = addr.call{value:1000}(abi.encodeWithSignature("echo()")); if(success){ return abi.decode(data,(string)); } else { return "error"; } } }
六、Solidity 自毁合约 selfdestruct
Solidity 自毁函数 selfdestruct 由以太坊智能合约提供,用于销毁区块链上的合约系统。
当合约执行自毁操作时,合约账户上剩余的以太币会强制发送给指定的目标,然后其存储和代码从状态中被移除。
所以,Solidity selfdestruct 做两件事。
- 它使合约变为无效,有效地删除该地址的字节码。
- 它把合约的所有资金强制发送到目标地址。
1.销毁合约示例
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Kill { function kill() external { selfdestruct(payable(msg.sender)); } function test() external pure returns(uint) { return 100; } }部署后,先调用 test 函数,将会输出 100。然后调用 kill 函数,再次调用 test 函数,结果输出为 0,表明合约被销毁。
2.强制发送资金示例
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Kill { function kill() external { selfdestruct(payable(msg.sender)); } function test() external pure returns(uint) { return 100; } }部署后,先调用 test 函数,将会输出 100。然后调用 kill 函数,再次调用 test 函数,结果输出为 0,表明合约被销毁。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Kill { constructor() payable {} function kill(address payable to) external { selfdestruct(to); } } contract Receive { function getBalance() external view returns(uint) { return address(this).balance; } }首先部署 Receive 合约,用于接收资金。再部署 Kill 合约,初始转入 Eth 123 wei,然后调用 kill 方法,并将 Receive 的地址作为参数。
我们通过 Receive 合约的 getBalance 方法查看余额,资金为 123 wei。
Receive 合约没有定义 fallback 和 receive 函数,正常情况下无法接收资金,但依然被 Receive 合约的 selfdestruct 方法强制转入了资金。
七、Solidity 哈希算法 keccak256
Solidity 的哈希算法使用一个内置函数 keccak256。
通常字符串不能进行比较,但是讲字符串进行哈希算法加密,即可进行字符串比较
keccak256函数原型:
keccak256(bytes) returns (bytes32)// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Hash { function hash(string memory _text, uint _num, address _addr) public pure returns (bytes32) { return keccak256(abi.encodePacked(_text, _num, _addr)); } }我们通常使用 abi.encodePacked 打包所有数据,然后再进行 keccak256 哈希。
但是,我们使用 abi.encodePacked 要非常小心,当将多个动态数据类型传递给 abi.encodePacked 时,可能会发生哈希冲突。
abi 编码函数除了 abi.encodePacked 外,还有函数 abi.encode。 abi.encodePacked 只是将参数转为 16 进制,再直接进行拼接,而 abi.encode 需要先进行补零 ,再进行转码拼接。
我们可以看一个例子:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Hash { function encode1() external pure returns(bytes memory){ return abi.encodePacked("aa","bb"); } function encode2() external pure returns(bytes memory){ return abi.encodePacked("aab","b"); } }两个方法返回的内容都是0x61616262,但两者的输入参数并不同。在这种情况下,您应该使用 abi.encode 代替。
或者使用 encodePacked,但是在两个参数之间再添加一个固定数字参数即可。
例如:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Hash { function encode1() external pure returns(bytes memory){ return abi.encodePacked("aa",uint(1),"bb"); } function encode2() external pure returns(bytes memory){ return abi.encodePacked("aab",uint(1),"b"); } }
八、Solidity 权限控制合约
Solidity 合约中一般会有多种针对不同数据的操作,例如对于存证内容的增加、更新及查询,所以需要制定一套符合要求的权限控制。
如何对合约的权限进行划分?我们针对Solidity语言设计了一套通过地址标记的解决方案。
合约中划分了角色和账户两级权限,如下所示:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Access { mapping(bytes32 =>mapping(address =>bool)) public roles; // 0xf23ec0bb4210edd5cba85afd05127efcd2fc6a781bfed49188da1081670b22d8 bytes32 constant private ADMIN = keccak256("admin"); // 0xcb61ad33d3763aed2bc16c0f57ff251ac638d3d03ab7550adfd3e166c2e7adb6 bytes32 constant private USER = keccak256("user"); // 授权合约部署者 ADMIN 权限 constructor() { _grantRole(ADMIN, msg.sender); } modifier onlyAdmin(address _account) { require(roles[ADMIN][_account], "not authorized"); _; } function _grantRole(bytes32 _role, address _account) internal { roles[_role][_account] = true; } // 授权 function grantRole(bytes32 _role, address _account) external onlyAdmin(_account) { _grantRole(_role, _account); } function _revokeRole(bytes32 _role, address _account) internal { roles[_role][_account] = false; } // 撤销授权 function revokeRole(bytes32 _role, address _account) external onlyAdmin(_account) { _revokeRole(_role, _account); } }
十、Solidity 验证签名
Solidity有一个
ecrecover
指令,可以根据消息hash
和签名,返回签名者的地址:ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address)根据恢复的签名地址,与验证地址对比,就可以验证签名。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Signature { function verify(address _signer, string memory _message, bytes memory _signature) external pure returns(bool) { bytes32 hash = getHash(_message); bytes32 ethSignedHash = getEthHash(hash); return recover(ethSignedHash,_signature) == _signer; } function getHash(string memory _message) public pure returns(bytes32) { return keccak256(abi.encodePacked(_message)); } function getEthHash(bytes32 _hash) public pure returns(bytes32) { return keccak256(abi.encodePacked("x19Ethereum Signed Message: 32", _hash)); } function recover(bytes32 ethSignedHash, bytes memory _signature) public pure returns(address) { (bytes32 r, bytes32 s, uint8 v) = _split(_signature); return ecrecover(ethSignedHash, v, r, s); } function _split(bytes memory _signature) internal pure returns(bytes32 r, bytes32 s, uint8 v){ require(_signature.length == 65, "invalid signaure length"); assembly { r := mload(add(_signature, 32)) s := mload(add(_signature, 64)) v := byte(0, mload(add(_signature, 96))) } } }
十一、ABI、bytecode、EVM
- ABI:接口 合约编译后会给出一个ABI地址,可以用于其他合约来实现(调用)接口
- bytecode:字节码 合约编译后会给出bytecode地址,字节码用来部署合约
- EVM:以太坊虚拟机
//以太坊虚拟机运行的是合约字节码,所以需要在部署到EVM前将代码先编译,编译成bytecode
//以太坊虚拟机是一个被沙箱封装起来、完全隔离的运行环境。运行在EVM内部的代码
//不能接触到网络、文件系统、或其他进程,即使同是智能合约,也只能进行有限交互
ABI
在以太坊生态系统中,应用程序二进制接口是从区块链外部与合约进行交互,以及合约合约之间进行交互的一种标准形式。简单来说就是以太坊调用合约时的接口说明,即定义操作函数签名、参数编码、返回结果编码等,但没有说明状态变量
ABI的系统函数
abi.encode(参数值) //计算参数值的abi编码
abi.encodeWithSignature(“函数名()”,参数,····) //计算函数和参数的ABI编码
十二、Solidity内置对象(全局变量、全局函数)
solidiy全局变量以及函数可认为是solidity提供的API,这些全局变量以及函数主要分为以下几类
API:接口
- 有关区块和交易的属性
- ABI编码函数
- 有关错误处理
- 有关数学和加密功能
- 地址相关
- 合约相关