适合有过编程经验的同学阅读
数据类型
布尔
bool public boo = true; // or false
整型
uint
无符号整型,有uint8
,uint16
等,uint8
的数据范围是int
整型,有int8
,int16
等,int8
的数据范围是
// uint 默认0
uint8 public u8 = 1;
uint public u256 = 456;
uint public u = 123; // uint 默认是 uint256
// int 默认0
int8 public i8 = type(int).min; // type(int).max
int public i256 = 456;
int public i = -123; // int 定义默认是 int256
地址
使用 address
声明,默认值为 0x0000000000000000000000000000000000000000
address public addr = 0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c;
Map
mapping
类型,为key-value
结构。支持嵌套
// mapping 定义
mapping(address => uint) public myMap;
myMap[_addr] = 0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c; // map赋值
mapping(address => mapping(uint => bool)) public nested; // 嵌套map
数组
array
数组类型, 支持方法 push
、pop
、get
、delete
等
// 数组定义
uint[] public arr;
uint[] public arr2 = [1, 2, 3];
uint[10] public myFixedSizeArr; // 固定数组长度
arr.length // 获取数组长度
arr2[1] // 访问数组元素
arr2[1] = 100 // 设置数组元素 [1, 100, 3]
delete arr2[1] // 删除数组元素 [1, 0, 3], 不会真正的删除,只会置空
枚举
enum
枚举类型
// 枚举定义
enum Status {
Pending,
Shipped,
Accepted,
Rejected,
Canceled
}
Status public status;
结构体
struct
结构体
// 结构体定义
struct Todo {
string text;
bool completed;
}
// 声明结构体类型变量
Todo public todo;
Todo[] public todos;
todo = Todo('a', true)
todo = Todo({ text: 'A', completed: true })
todo.completed = false;
接口
使用 interface
声明,声明一个接口时,需要遵循以下要求
- 不能有函数实现
- 可以从其他接口继承
- 所有声明的函数必须声明为
external
- 不能声明构造函数
- 不能声明状态变量
interface UniswapV2Factory {
function getPair(address tokenA, address tokenB) external view returns (address pair);
}
interface UniswapV2Pair {
function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
}
函数
函数定义
符合格式: function 函数名(参数类型 参数名) 修饰符 returns(返回值类型) {}
参数名作为约定,其前需带下划线
function add (uint _a, uint _b) returns(uint){
return a + b;
}
返回值
- 使用关键字
returns
确定返回值 - 支持多返回值
returns
支持赋予返回值值名称,此时不需要显示指定return
multipleReturns
可以不写关键字returns
function returnMany() public pure returns(uint, bool, uint) {
return (1, true, 2);
}
// 隐式返回
function assigned() public pure returns(uint x, bool b, uint y) {
x = 10;
b = false;
y = 10;
}
function multipleReturns () {
uint a;
uint b;
uint c;
(a, b, c) = multipleReturns()
}
Modifier
Modifier
是可以在函数调用之前运行的代码, 相当于函数的钩子
// 定义 modifier 要求执行人必须是合约的所有者
// _;表示执行函数体内的代码
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_; // 调用 onlyOwner 的函数的内部代码
}
modifier validAddress(address _addr) {
require(_addr != address(0), "Not valid address");
_;
}
// 执行该函数前会先去执行 onlyOwner 和 validAddress, 如果都通过则才会去执行后续代码
function changeOwner(address _newOwner) public onlyOwner validAddress(_newOwner) {
owner = _newOwner;
}
pure / view
-
view
告诉我们运行这个函数不会更改和保存任何数据, 用于读取链上数据 -
pure
告诉我们这个函数不但不会往区块链写数据,它甚至不从区块链读取数据。
这两种在被从合约外部调用的时候都不花费任何 gas
contract ViewAndPure {
uint public x = 1;
// 没有修改链上的数据x, 仅仅只是读取x的值
function addToX(uint y) public view returns (uint) {
return x + y;
}
// 没有修改链上的数据, 也没有读取链上的值x
function add(uint i, uint j) public pure returns (uint) {
return i + j;
}
}
关键字
Solidity 中的关键字包括了
- 可见性
- 常量
- 不可变量
- 数据存储位置
- 可支付
可见性
关键字 | 说明 |
---|---|
private | 只能被合约内部调用 |
public | 可以在任何地方调用,不管是内部还是外部 |
internal | 能被合约内部调用,还能被继承的合约调用 |
external | 只能从合约外部调用 |
常量
使用 constant
修饰常量,修饰的变量需要在编译期确定值, 链上不会为这个变量分配存储空间, 它会在编译时用具体的值替代, 大写变量名
address public constant MY_ADDRESS = 0x777788889999AaAAbBbbCcccddDdeeeEfFFfCcCc;
不可变量
immutable
修饰的变量是在部署的时候确定变量的值, 它在构造函数中赋值一次之后就不在改变, 这是一个运行时赋值, 可以解除之前 constant
不支持使用运行时状态赋值的限制
// 不可变量
contract Immutable {
// coding convention to uppercase constant variables
address public immutable MY_ADDRESS;
constructor(uint _myUint) {
MY_ADDRESS = msg.sender;
}
}
数据存储位置
用 storage
、memory
、calldata
修饰的变量
storage
变量是状态变量(存储在区块链上), 赋值时是浅拷贝memory
变量在内存中, 不上链, 并且在调用函数时存在, 赋值时是深拷贝calldata
变量在内存中, 不上链, 不能修改, 一般用于函数的参数。相对于 memory, 可以节省 gas
function awardItem(address _to, string memory _tokenURI) public returns(uint) {
uint newItemId = _tokenIds.current();
_mint(_to, newItemId);
_setTokenURI(newItemId, _tokenURI);
_tokenIds.increment();
return newItemId;
}
可支付
函数和地址能声明为 payable
, 即可接受 ether
到智能合约
uint levelUpFee = 0.001 ether;
function levelUp(uint _zombieId) external payable {
require(msg.value == levelUpFee);
zombies[_zombieId].level++;
}
调用该函数需要支付 0.001 ether
。在你发送 ether
之后,被存储进以合约的以太坊账户中。 除非你在合约中添加一个提现函数来从合约中提取资产。
function withdraw() external onlyOwner {
// 获取合约账户的余额
uint balance = address(this).balance;
if (balance > 0) {
// 转账到 owner 账户
payable(owner()).transfer(balance);
emit Withdraw(owner(), balance);
}
}
转移合约中的资产有以下三种方式
payable(_to).transfer(balance);
bool sent = payable(_to).send(balance);
(bool sent, bytes memory data) = _to.call{ value: balance }('');
智能合约基础
创建智能合约
创建智能合约之前需要声明 SPDX 协议
软件包数据交换(SPDX) 规 范定义了一个用于交流软件组件信息的开放标准,具体查看 licenses
其实还要声明 solidity
的版本
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract X {}
构造函数
只在合约部署的时候调用一次,之后便不再调用
contract X {
string public name;
address public owner;
constructor(string memory _name) {
name = _name;
owner = msg.sender;
}
}
变量
- 状态变量: 存储在链上的变量,所有合约内函数都可以访问,
gas
消耗高,声明在合约内、函数外 - 局部变量: 局部变量是仅在函数执行过程中有效的变量,函数退出后,变量无效。局部变量的数据存储在内存里,不上链,
gas
低 - 全局变量: 全局变量是全局范围工作的变量,都是
solidity
预留关键字。他们可以在函数内不声明直接使用。查看msg.sender
block.number
msg.data
继承
- 支持多继承
- 重写父级函数
- 父合约中的方法加上了
virtual
则表示子合约可以重写 - 子合约重写父合约中的方法时需加上
override
关键字 - 多继承时,某个方法同时存在于多个父合约
- 父合约中的方法加上了
- 调用父级函数:
super.func()
或者父合约名.func()
- 子合约中不能通过声明相同的变量来覆盖父级合约中的变量
// 创建合约X
contract X {
string public name;
constructor(string memory _name) {
name = _name;
}
// 定义可重写方法
function getName() public virtual view returns(string memory){
return name;
}
}
// 创建合约Y
contract Y {
string public text;
constructor(string memory _text) {
text = _text;
}
}
// 多继承,并通过参数形式调用父级合约的构造函数
contract B is X("Input to X"), Y("Input to Y") {
// 重写父级函数
function getName() public override view returns(string memory){
// 调用父级函数
return super.getName();
}
}
// 多继承, 子合约在构造函数调用父级构造函数,可以指定调用顺序,为 X, Y, C
contract C is X, Y {
constructor(string memory _name, string memory _text) X(_name) Y(_text) {}
}
特性
合约间交互
合约允许与链上的其他合约进行交互
假设存在链上合约 LuckyNumber
contract LuckyNumber {
mapping(address => uint) numbers;
function setNum(uint _num) public {
numbers[msg.sender] = _num;
}
function getNum(address _myAddress) public view returns (uint) {
return numbers[_myAddress];
}
}
另一个合约希望访问 LuckyNumber
中的数据,需要先定义其 Interface
interface LuckyNumberInterface {
function getNum(address _myAddress) public view returns (uint);
}
contract MyContract {
// LuckyNumber 的合约地址
address NumberInterfaceAddress = 0xab38...;
// numberContract 指向一个合约对象
LuckyNumberInterface numberContract = NumberInterface(NumberInterfaceAddress);
function someFunction() public {
// 访问合约方法
uint num = numberContract.getNum(msg.sender);
}
}
发送主币
三种方式
transfer
send
call
contract SendEther {
contract() payable {}
receive() external payable {}
function send(address payable _to) external payable {
// _to.transfer(123);
// bool sended = _to.send(123)
// (bool sended, ) = _to.call{value: 123}("")
}
}
接收主币
receive
- 接收主币时调用,一个合约最多只能存在一个该函数。声明方式与一般函数不一样,不需要
function
关键字 receive()
函数不能有任何的参数,不能返回任何值,必须包含external
和payable
- 需要注意的是用户往该合约发送主币时,
gas
会被限制在2300
,如果receive
执行过多的逻辑,可能会出现Out of Gas
错误
contract ReceiveEther {
event ReceivedEvent(address sender, uint value);
receive () external payable {
emit ReceivedEvent(msg.sender, msg.value);
}
}
fallback
- 调用不存在的函数
- 主币被直接发送到合约,但
receive()
不存在或msg.data
不为空
contract ReceiveEther {
event FallbackedEvent(address sender, uint value, bytes data);
fallback () external payable {
emit FallbackedEvent(msg.sender, msg.value, msg.data);
}
}
receive
和 fallback
的执行选择
- 存在
msg.data
调用fallback
- 不存在
msg.data
- 存在
receive
则调用receive
- 不存在
reveive
则调用fallback
- 存在
若 reveive
和 fallback
都不存在, 则会报错
异常处理
try...catch... / throw
function tryCatchExternalCall(uint _i) public {
try foo.myFunc(_i) returns (string memory result) {
// ...
} catch {
// ...
}
}
require / revert / assert
contract A {
function testRequire(uint _i) public pure {
require(_i > 10, '_i error'); // 当 _i <= 10报错
}
function testRevert(uint _i) public pure {
if (_i > 10) {
revert('i need to be greater than 10'); // 不满足条件主动触发错误
}
}
function testAssert(uint _i) public pure {
assert(_i == 100); // 不满足assert内部条件的报错
}
}
自定义错误
contract A {
// 定义错误
error MyError(address caller, uint i);
function testCustomeError(uint _i) public view {
if (_i > 10) {
revert MyError(msg.sender, _i)
}
}
}
模块化
import
使用 import
导入合约,并使用导入的合约的内容。 import
可以使本地合约路径或远程地址
// A.sol
contract A {}
// B.sol
import "./A.sol"; // 导入合约A
contract B is A {}
library
库类似于合约,但你不能声明任何状态变量,也不能发送以太币。如果所有库函数都是 internal
的,则将库嵌入到合约中。
使用库合约需要使用 using for
, 如下所示
// safemath.sol
library SafeMath {
function add(uint x, uint y) internal pure returns (uint) {
uint z = x + y;
require(z >= x, "uint overflow");
return z;
}
}
// TestSafeMath.sol
import "./safemath.sol";
contract TestSafeMath {
// 使用 SafeMath, 则 uint 会拥有 SafeMath 所有方法
using SafeMath for uint;
uint public MAX_UINT = 2**256 - 1;
function testAdd(uint x, uint y) public pure returns (uint) {
return x.add(y);
}
function testSquareRoot(uint x) public pure returns (uint) {
return Math.sqrt(x);
}
}
unchecked
0.8.0 版本开始,算术运算有两个计算模式:
unchecked
模式,即在发生溢出的情况下会进行“截断”,不会触发失败异常,从而得靠引入额外的检查库来解决这个问题(如 OpenZeppelin 中的 SafeMath 库)checked
模式,默认情况下,会进行溢出检查,如果结果溢出,会出现失败异常回退。
contract TestContract {
function checkedTest() external pure returns(uint256) {
// 溢出 会触发异常
uint256 x = 0;
x--;
return x;
}
function uncheckedTest() external pure returns(uint256) {
// 不会报错
uint256 x = 0;
unchecked { x--; }
return x;
}
}
create / create2
用于在合约内创建合约。
create
创建合约是通过new
构造合约实例,并传入构造参数
// value 表示可以payable的构造函数转入的eth数量
Contract con = new Contract{value: _value}(params);
address(con); // 合约的地址
create
合约地址是通过交易发起者sender
的地址以及交易序号nonce
来计算确定的。sender
和 nonce
进行 RLP
编码,然后用 Keccak-256
进行 hash
计算(伪码)
address = keccak256(rlp([sender, nonce]));
创建者地址不会变,但nonce
可能会随时间而改变,因此用create
创建的合约地址不好预测。
create2
创建合约也是通过new
构造合约实例,不同的是需要一个 salt
参数
Contract con = new Contract{salt: _salt, value: _value}(params)
用create2
创建的合约地址由 4 个部分决定:
0xFF
:常数- 交易发起者
sender
的地址 salt
:给定的数值- 待部署合约的字节码(
bytecode
)
address = keccak256(0xFF + sender + salt + bytecode)
因此可以事先计算出create2
合约地址
address(uint(keccak256(abi.encodePacked(
hex'ff',
address(this),
salt,
keccak256(type(Contract).creationCode)
))));
call
用来与其他合约进行交互,返回值为 (bool success, bytes memory data)
, success
表示调用是否成功,data
表示调用的函数的返回值
使用方式如下:
contractAddress.call(abi.encodeWithSignature("函数名(...参数类型)", ...params));
如果调用的合约方法是可交易的,则可以指定发送的 eth 数量
contractAddress.call{ value: _value, gas: _gas }(abi.encodeWithSignature(...))
例如
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Receiver {
uint256 private _value = 0;
event Received(address caller, uint amount, string message);
fallback() external payable {
emit Received(msg.sender, msg.value, "Fallback was called");
}
// 设置 _value 的值
function store(uint256 newValue) public payable returns(uint){
_value = newValue;
emit Received(msg.sender, msg.value);
return newValue + 1;
}
// 读取 _value 的值
function retrieve() public view returns (uint256) {
return _value;
}
}
contract Caller {
event Response(bool success, bytes data);
function testCall(address payable _addr) public payable {
// 调用 Receiver 合约的 store 函数,修改 Receiver 中的变量 _value 的值为88
(bool success, bytes memory data) = _addr.call{value: msg.value}(
abi.encodeWithSignature("store(uint256)", 88)
);
emit Response(success, data);
}
}
delegatecall
委托调用,顾名思义就是委托别人去调用,和 call
的区别见下图
通过 delegatecall
调用合约 B
函数时,由于此时上下文是处于合约 A
的,所以函数执行所影响的状态是合约 A
的。
调用方式和 call
类似
contractAddress.delegatecall(abi.encodeWithSignature("函数名(...参数类型)", ...params));
示例:
contract Receiver {
uint256 private _value = 0;
function store(uint256 newValue) public {
_value = newValue;
}
function retrieve() public view returns (uint256) {
return _value;
}
}
contract Caller {
uint256 public _value = 0;
function testDelegateCall(address payable _addr) public payable {
// 通过 delegatecall 调用 Receiver 合约中的 store方法时
// 由于 Receiver 合约此时所处的上下文是在 Caller 合约中。所以对 _value 的赋值是 Caller 中的。
// 导致的最终结果就是 Receiver 中 _value 值仍为0, Caller 中 _value 值为88
(bool success, ) = _addr.delegatecall(
abi.encodeWithSignature("store(uint256)", 88)
);
}
}
selector
当调用合约中的函数 时会传入一段 calldata
,其值说明了要调用的函数名以及传入的参数。例如调用 store(10)
时,其 calldata
如下所示
0x6057361d000000000000000000000000000000000000000000000000000000000000000a
前 4 个字节 0x6057361d
为函数选择器,也叫 selector
,由 keccak256
计算得到
bytes4(keccak256("store(uint256)")); // 0x6057361d
selector
后面的值为函数参数。
当通过 call
调用时就可以使用 selector
指定方法
(bool success, bytes memory data) = _addr.call{value: msg.value}(
abi.encodeWithSelector(0x6057361d, 88)
);