Solidity开发指南(完整版)/ 学习智能合约#68
Solidity开发指南,包括Solidity合约语言,使用方法和案例。
github地址
- 下载与资源
- 数据类型
- 值类型
- 引用类型
- 变量修饰符
- 数据类型小结
- 货币单位
- 伪随机数
- 时间单位
- 全局变量和函数
- 函数
- 接受函数和回退函数
- 访问权限
- 函数修饰符
- 发送以太币的方法
- 存储位置
- memory和storage的区别
- 函数修饰器modifier
- 函数选择器Selector
- 导入其他源文件
- 库
- 错误异常
- 自定义错误
- delete
- 销毁合约
- 事件Events
- 合约元素
- 第一个合约
- 简单代币合约
- ERC20
- 以太坊+IPFS数据存储可行方案
- 事件日志event
- call和delegatecall
- 底层调用calldata
- 抽象合约
- 接口
- interface id
- ABI
- 数学和哈希函数
- 签名参数
- 验证签名
- 投票合约
- 转帐合约
- 批量转帐合约
- ICO合约
- 买卖合约
- SharesPool挖矿算法
- 权限控制
- 判断字符串相等
- virtual与override
- super关键字
- tx.origin
- factory
- 存证合约
- SafeMath
- storage类型的坑点
- 一次性获取数组或结构体的所有数据
- 重入攻击
- 防御重入攻击
- 汇编assembly
- 代理合约升级
- multicall
- 节约gas
- merkle tree proof
- graph
- 监听合约事件
下载与资源
以太坊中文手册 |
geth |
solidity手册 |
登链手册 |
openzeppelin |
案例集 |
remix |
MetaMask |
MEW钱包 |
xilibi |
ASCii码表 |
chainlist |
水龙头 |
Solidity - 0.8.30
数据类型
值类型
值类型传值时,会临时拷贝一份内容出来,而不是拷贝指针,当你修改新的变量时,不会影响原来的变量的值。
布尔(Booleans)
整型(Integer)
地址(Address)
定长字节数组(fixed byte arrays)
有理数和整型(Rational and Integer Literals,String literals)
枚举类型(Enums)
函数(Function Types
引用类型(Reference Types)
引用即地址传递,复杂类型,占用空间较大。在拷贝时占用空间较大,所以考虑通过引用传递。
不定长字节数组(bytes)
字符串(string)
数组(Array)
结构体(Struts)
两者区别:
如果是值传递,修改新变量时,不会影响原来的变量值,如果是引用传递,那么当你修改新变量时,原来变量的值会跟着变化,这是因为新变量同时指向同一个地址的原因。
值类型
布尔 true false
return a && b;
return a || b;整形 integer
int 有符号 uint 无符号
int8~int256 默认int256 uint256, 后面的数字为占空间的大小
int8 8位整型
int256 256位整型
int = int256
uint8 8位无符号整型(正整数)
uint8范围: 0~255
比较运算符 < =
位操作符 & | ~
三元运算: x < 10 ? 1 : 2;
整型溢出问题
uint8最大值是255,最小值是0。超出最大值或小于最小值则会溢出。
高版本已无需考虑溢出了。
地址 address
表示一个帐户或合约地址,20字节,有一般地址和可支付地址。
在十六进制中一个字节占两位,
以太坊地址通常是42个字符长,以“0x”开头,后面跟随40个十六进制字符(0-9, a-f)
所以钱包地址ca35b7d915458ef540ade6068dfe2f44e8fa733c的长度为40。
address:保存一个20字节的值(以太坊地址的大小)。
address payable :可支付地址,与 address 相同,不过有成员函数 transfer 和 send 。
属性:balance payable
方法:send() transfer() call() sendValue()
全局变量:msg.sender //调用合约方法的人
msg.value //附带的以太币
this //合约指针枚举 enums
枚举可用来创建由一定数量的“常量值”构成的自定义类型,默认为uint8,起始为0,多用于判断条件。
举例1
举例2
// 使用枚举自定义一个类型 ActionChoices
enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill }
// 定义一个ActionChoices类型的变量
ActionChoices choice;
//定义类型时可以直接赋值
ActionChoices defaultChoice = ActionChoices.GoStraight;
函数function
外部和内部函数 external internal
合约内部可以直接调用内部函数,不能调用外部函数。定长字节数组
定长数组 bytes1~bytes32
字节不可修改,长度不可修改
可以像字符串一样使用
像整型一样比较和运算
像数组一样索引
bytes1 bytes2 ... bytes32
1字节等于8位二进制 2位十六进制
一个英文字符等于一个字节,一个中文(含繁体)等于三个字节。中文标点占三个字节,英文标点占一个字节
byte = bytes1
bytes1 a = 0xb5; // [10110101]
- 常量举例
常量
引用类型
- 映射 mapping
类似python字典,主要用于存储。
mapping(address => uint) public balances;
balances[userAddr]访问
mapping(uint => mapping(string => uint)) public people; //嵌套定义
mapping (bytes => Employee) bytesMapping; //字节数组作为key
mapping (address => Employee) addressMapping; // address作为key
mapping (string => mapping(uint => Employee)) complexMapping; // mapping作为value,是可以的。
//mapping可以设置id自增长以实现遍历。
uint public id = 0;
struct Article {
string hash;
address authoraddr;
address[] voted;
uint[] amount;
}
mapping(uint => Article) public articles; //设置uint为id
event PostArticle(uint id, string t);
function postArticle(string memory _hash) public {
id ++; //id自增长,通过它来实现遍历。
articles[id].hash = _hash;
articles[id].authoraddr = msg.sender;
emit PostArticle(id, _hash);
}
- 结构体 struct
结构体是可以将几个变量分组的自定义数据类型
struct Student {
string name;
uint age;
uint score;
string sex;
}
Student stu1 = Student("lili", 18, 60, "girl");
Student stu2 = Student({name:"jim", age:20, score:80, sex:"boy"});
//可以存入数组以实现遍历
Student[] students;
- 字符串
存储utf-8编码的字符串数据
string a;
string public str1 = "hello world"
name = "" //空字符串
不能直接下标访问,没有length
solidity字符串功能相当弱小,要导入别的库
import "github.com/Arachnid/solidity-stringutils/strings.sol";
- 不定长字节数组,内容和长度均可修改
存储任意长度的字节数据
bytes bs;
bytes a = "hello"
可赋值,可动态调节,push,length
bytes public name = "helloworld";
bytes string可自由转换(bytes1不能直接转成sting, 需转成bytes):
bytes("helloworld")
string(bytes)
- 数组
string, bytes, bytes1~bytes32本质上都是数组
分为固定长度数组和动态长度数组
固定长度数组,直接赋值,length
uint[7] arr = [1,2,3,4,5,6,7];
arr[0]
动态长度数组, length,push
uint[] arr = [1,2,3,4,5,6,7];
二维数组,先列,再是行
uint8[3][2] arrays = [[1,2,3], [2,3,5]]
//可使用new关键字创建一个memory的数组,可以是任意的类型,地址、结构体、字符串等
returnData = new Delegator[](delegatorList.length);
function func1() public {
uint[] memory v1 = new uint[](10);
v1[0] = 1;
}
uint[] public arr1; //storage
function func2() public {
arr1 = new uint[](10);
arr1[0] = 2;
arr1.push(15);
arr1.length;
}
uint [10] tens;
uint [] us;
uint [] public u = [1, 2, 3]; // 生成函数
uint[] public b = new uint[](7); //storage
.lengh获取长度
.push添加元素(memory数组不支持)
删除数组:没有删除指定元素的方法,只有.pop删除最后一个元素。
或者只能用delete将其重置为0。
delete arrays;
delete addrs[_a]; // 等价于: addrs[_a] = address(0)
- 合约类型
import一个合约A后,在新合约中, A可以当做一个合约类型来使用,
不过A中的函数执行环境不变,仍在A合约中执行。例如:B中导入A,A.send()仍会在A的环境中执行。
eg:
contract A {
function add (uint x, uint y) public pure returns (uint) {
return x.plus(y);
}
}
import "./A.sol"
contract B {
function add2 (A a, uint x, uint y) public pure returns (uint) {
return a.add(x,y)
}
}
变量修饰符
constant, immutable
constant 修饰的变量需要在编译期确定值, 链上不会为这个变量分配存储空间, 它会在编译时用具体的值替代, 因此, constant常量是不支持使用运行时状态赋值的(例如: block.number, block.timestamp, msg.sender 等这些是不能用constant )。
constant 目前仅支持修饰 strings 及 值类型. 它更为节约gas
uint public constant NUM = 69;
immutable 修饰的变量是在部署的时候确定变量的值, 它在构造函数中赋值一次之后,就不再改变, 这是一个运行时赋值, 就可以解除之前 constant 不支持使用运行时状态赋值的限制.
uint immutable decimals;
uint immutable maxBalance;
IMasterChef public immutable MASTER_CHEF;
address public immutable owner = msg.sender;
数据类型小结
货币单位
最小单位是wei
ether wei (finney szabo 7.0后已不再使用)
1 ether = 10**18 wei // 1e18
1 Gwei = 10**9 wei
伪随机数
solidity中并没有真正的随机数,不建议使用,最好使用预言机中的随机数功能。
random = uint256(keccak256(abi.encode(block.timestamp, admin, hash))) % 100;
eg:
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
function _generateRandomDna(string memory _str) public view returns (uint) {
return uint(keccak256(abi.encodePacked(_str, block.timestamp))) % dnaModulus;
//9758857365005908
}
时间单位
默认单位是秒
block.timestamp //1592697341 单位为秒的时间戳
1 == 1 seconds
1 minutes == 60 seconds
1 hours == 60 minutes
1 days == 24 hours
1 weeks == 7 days
eg:
return x * 1 hours + y * 1 minutes; //3660
全局变量和函数
在全局命名空间中已经存在了(预设了)一些特殊的变量和函数,他们主要用来提供关于区块链的信息或一些通用的工具函数。可以把这些变量和函数理解为Solidity语言层面的(原生)API 。
blockhash(uint) :指定区块的区块哈希——仅可用于最新的 256 个区块且不包括当前区块
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.gas (uint): 剩余的gas量
msg.sender ( address ): 消息发送者(当前调用)
msg.sig ( bytes4 ): calldata 的前4字节(也就是函数标识符)
msg.value ( uint ): 随消息发送的 wei 的数量
tx.gasprice (uint): 交易的 gas 价格
tx.origin (address payable): 交易发起者(完全的调用链)
//合约相关
this : 表示当前合约,可以转换为地址
selfdestruct(address recipient) 销毁合约,并把它所有资⾦发送到给定的地址recipient
函数
函数是完成特定任务的自包含代码模块。与其他 web3 编程语言一样,Solidity 允许开发者通过使用函数编写模块化代码,以消除重新编写相同代码片段的冗余。相反,开发者可以在程序中必要时调用该函数。
function function-name(parankust...) modifiers returns(returnlist...) {
// statements
}
// 使用 function 关键字定义函数
// 创建一个唯一的函数名称,且不与任何保留关键字冲突
// 列出包含参数名称和数据类型的参数,或者不包含任何额外参数
// 创建一个用大括号包围的语句块
接受函数和回退函数
receive接收以太币, 无名称,无参数,无返回值
一个合约最多有一个 receive 函数, 声明函数为:
receive() external payable {}
Fallback 回退函数,无名称,无参数,无返回值
合约可以最多有一个回退函数。函数声明为:
fallback () external [payable] {}
receive() external payable {
totalAmount += msg.value;
addrs.push(msg.sender);
}
fallback() external payable {
totalAmount += msg.value;
addrs.push(msg.sender);
}
访问权限
public private external internal
public - 任意访问
private - 仅当前合约内
internal - 仅当前合约及所继承的合约
external - 仅外部访问(在内部也只能用外部访问方式访问)
当使用public 函数时,Solidity会立即复制数组参数到内存, 而external函数则是从calldata读取,而分配内存开销比直接从calldata读取要大的多。
那为什么public函数要复制数组参数数据到内存呢?是因为public函数可能会被内部调用,而内部调用数组的参数是当做指向一块内存的指针。
对于external函数不允许内部调用,它直接从calldata读取数据,省去了复制的过程。
所以,如果确认一个函数仅仅在外部访问,请用external。
当需要内外部都要调用的时候,请用public。
public external
都可以访问,都可以继承
但是external内部不能访问,除非带上this
private internal
只能内部访问,private不可继承,internal可以继承
函数修饰符
payable view pure:
payable 可以接受以太币
view 只看不修改(状态变量)
pure 纯函数,不读也不写(状态变量)
发送以太币的方法
transfer 和 send异同:
两者都可以转移以太币,但尽量使用transfer。都有2300gas的限制。
在转帐时send异常时不会发生错误,只会返回一个布尔值(false),所以要使用判断函数assert(addr.send(1 ether))
//有2300gas的限制
_to.transfer(1 ether);
bool sent = _to.send(1 ether);
require(sent, "Failed to send Ether");
(bool sent, bytes memory data) = _to.call{value: 1 ether}("");
require(sent, "Failed to send Ether");
//sendValue没有gas的限制
owner.sendValue(address(this).balance);
function sendValue(address payable recipient, uint256 amount) internal {
require(address(this).balance >= amount, "Address: insufficient balance");
// solhint-disable-next-line avoid-low-level-calls, avoid-call-value
(bool success, ) = recipient.call{ value: amount }("");
require(success, "Address: unable to send value, recipient may have reverted");
}
存储位置
memory storage calldata
memory 存储在EVM内存中,主要有局部变量,函数参数,值传递
storage 存储在区块链中,主要有状态变量,复杂变量,数组,引用传递,指针
calldata,用来存储函数参数,是只读的,不会永久存储的一个数据位置。外部函数的参数(不包括返回参数)被强制指定为calldata。效果与memory差不多,但更为节约gas。
int[] arr;
string tt;
function fun1(uint m, string memory s) public returns(string memory) {
uint n = m;
string memory str = s;
tt = s;
string memory s1 = 'abc';
string memory s2 = s1;
int[] storage abc = arr;
return tt;
}
function fun2(uint m, string calldata s) external {
uint n = m;
string memory str = s;
}
memory和storage的区别
memory 值传递,临时变量,EVM内存中
storage 引用传递,指针,会改变原变量的值,区块链中
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract storage_demo {
struct User {
string name;
uint8 age;
}
User adminuser;
function setUser(string memory _name, uint8 _age) public {
adminuser.name = _name;
adminuser.age = _age;
}
function getUser() public view returns (User memory) {
return adminuser;
}
function setAge1(uint8 _age) public {
User memory user = adminuser; //改变不了adminuser
user.age = _age;
}
function setAge2(uint8 _age) public {
User storage user = adminuser; //可以改变adminuser
user.age = _age;
}
function setAge3(User storage _user, uint8 _age) internal {
_user.age = _age;
}
function callsetAge3(uint8 _age) public {
setAge3(adminuser, _age); //可以改变adminuser
}
}
函数修饰器modifier
使用修饰器modifier可以轻松改变函数的行为。 例如,它们可以在执行函数之前自动检查某个条件。
contract owned {
address owner;
//修饰器所修饰的函数体会被插入到特殊符号 _; 的位置。
modifier onlyOwner {
require(
msg.sender == owner,
"Only owner can call this function."
);
_;
}
//只有合约的创建者才能销毁合约
function destroy() public onlyOwner {
selfdestruct(owner);
}
}
函数选择器Selector
abi.encodeWithSignature(....)的前四个字节就是函数选择器,也就是msg.sig,
也可这这样计算 bytes4(keccak256(bytes(_func)))
这种方法可以直接特定合约的方法
eg:
addr.call(abi.encodeWithSignature("transfer(address,uint256)", SomeAddress, 123))
bytes4(keccak256("set(uint256)"))
eg2:
function foo(string memory _message, uint _x) public payable returns (uint) {
emit Received(msg.sender, msg.value, _message);
return _x + 1;
}
(bool success, bytes memory data) = _addr.call{value: msg.value, gas: 5000}(
abi.encodeWithSignature("foo(string,uint256)", "call foo", 123)
);
导入其他源文件
import * as symbolName from "filename";
import "filename" as symbolName;
然后所有函数都以symbolName.symbol格式提供。
import 相当于接口,导入其它合约的函数,不过函数的执行环境不变,仍在原有合约中执行。例如:B中导入A,A.send()仍会在A的环境中执行。
import 演化成库和接口(interface), 标准化程度更高。
eg:
contract A {
function add (uint x, uint y) public pure returns (uint) {
return x.plus(y);
}
}
import "./A.sol"
contract B {
function add2 (A a, uint x, uint y) public pure returns (uint) {
return a.add(x,y)
}
}
库
library, 标准的函数,可以反复使用。调用时类似delegatecall
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
library mathlib {
function plus(uint a, uint b) public pure returns (uint) {
uint c = a + b;
assert(c>=a && c>=b);
return c;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./mathlib.sol";
contract testLib {
using mathlib for uint;
function add (uint x, uint y) public pure returns (uint) {
return x.plus(y);
// return mathlib.plus(x,y);
}
}
//使用有两种方法:
一是直接使用:mathlib.plus(x,y)
二是拓展类型:
using mathlib for uint;
//using mathlib for uint[]
x.plus(y);
错误异常
Assert, Require, Revert
assert(bool condition)
⽤于判断内部错误,条件不满⾜时抛出异常.函数只能用于测试内部错误,并检查非变量。
用于pure函数,会对用户惩罚,扣光gas
require(bool condition)
require(bool condition, string message) //提供错误信息。
函数用于确认条件有效性,例如输入变量,或合约状态变量是否满足条件,或验证外部合约调用返回的值。⽤于判断输⼊或外部组件错误,条件不满⾜时抛出异常。
会退还剩余gas
revert() //终⽌执⾏并还原改变的状态
revert(string reason) //提供⼀个错误信息。
可以用来标记错误并回退当前的调用。 revert 调用中还可以包含有关错误信息的参数,这个信息会被返回给调用者。
require(msg.value % 2 == 0, "Even value required.");
assert(this.balance == balanceBeforeTransfer - msg.value / 2);
if (amount > msg.value / 2 ether){
revert("Not enough Ether provided.");
}
自定义错误
error Myerror(address caller, uint i);
function test(uint _i) public view {
if(_i > 10) {
revert Myerror(msg.sender, _i);
}
}
delete
delete操作符可以用于任何变量,将其设置成默认值。
删除字符串时,会将其值重置为空。
删除枚举类型时,会将其值重置为序号为0的值。
如果对动态数组使用delete,则删除所有元素,其长度变为0。
如果对静态数组使用delete,则重置所有索引。
如果对map类型使用delete,什么都不会发生。
如果对map类型中的一个键使用delete,则会删除与该键相关的值。
eg:
uint256 public number = 20;
address[] public addrs;
delete number;
//number = 0;
delete addrs[1];
//addrs[1] = address(0); delete将对应数组中的元素重置为0地址。
contract DeleteDemo{
bool public b = true;
uint public i = 1;
address public addr = msg.sender;
bytes public varByte = "123";
string public str = "abc";
enum Color{RED,GREEN,YELLOW}
Color public color = Color.GREEN;
function deleteAttr() public {
delete b; // false
delete i; // 0
delete addr; // 0x0
delete varByte; // 0x
delete str; // ""
delete color;//Color.RED
}
}
销毁合约
合约代码从区块链上移除的唯一方式是合约在合约地址上的执行自毁操作 selfdestruct 。合约账户上剩余的以太币会发送给指定的目标,然后其存储和代码从状态中被移除。移除一个合约听上去不错,但其实有潜在的危险,如果有人发送以太币到移除的合约,这些以太币将永远丢失。
尽管一个合约的代码中没有显式地调用 selfdestruct ,它仍然有可能通过 delegatecall 或 callcode 执行自毁操作。
function destroy() public onlyOwner {
selfdestruct(owner);
}
事件Events
事件是合约外部通知,在logs中查看。一般用于dapp监听使用。
事件在合约中可被继承。当他们被调用时,会使参数被存储到交易的日志中 —— 一种区块链中的特殊数据结构。
成本较低
event Set(uint value);
emit Set(x); //触发事件
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract EventTest {
event LogEvent(string indexed a, uint8 b);
event TestEvent(string a);
function eventFunction() public{
emit LogEvent("hello", 101);
}
function test() public{
emit TestEvent("world");
}
function getSig() public pure returns(bytes32, bytes32){
bytes32 r1 = keccak256("TestEvent(string)"); //事件topic的值
bytes32 r2 = keccak256("LogEvent(string,uint8)");
return(r1, r2);
}
}
其中 keccak256("TestEvent(string)"),即事件的签名,也是事件topic的值。
//logs-LogEvent
{
"from": "0xa514f9ce4ceed99e4731b437123073f2d0c1745c",
"topic": "0x449e54c0703954de7e4a92b7f921b71b2574e355474bdcfe8461f34d63e1e542",
"event": "LogEvent",
"args": {
"0": {
"hash": "0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8",
"type": "Indexed"
},
"1": 101,
"a": {
"hash": "0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8",
"type": "Indexed"
},
"b": 101,
"length": 2
}
}
//logs-TestEvent
{
"from": "0xa514f9ce4ceed99e4731b437123073f2d0c1745c",
"topic": "0xe75028ff36bb6473da3731a30e1aeeae9988e2415dba2c4e91e0357955065fba",
"event": "TestEvent",
"args": {
"0": "world",
"a": "world",
"length": 1
}
}
合约元素
- 版本申明
- 引用
- 合约主体
- 注释
// SPDX-License-Identifier: MIT //开源协议
pragma solidity ^0.8.20;//申明版本,最低0.8.20
/*
* 这是注释段落
*
*/
import "./first_interface.sol"; //引用文件
contract SimpleStorage {
//合约主体
uint storedData;
function set(uint x) public {
storedData = x;
}
function get() public view returns (uint) {
return storedData;
}
}
Leave Solidity开发指南(完整版)/ 学习智能合约#68 to:
Read more #cn posts
Best Posts From lemooljiang
We have not curated any of lemooljiang's posts yet. But you can encourage our curation team to review posts by visiting them regularly and by referring other readers. Because we give priority to frequently read content.