Solidity-101
课程主页: https://www.wtf.academy/zh/course/solidity101
- sol文件第一行为注释license,第二行为pragma,第三行为contract
- 类型: bool, int, uint, int256, uint256, address(20 byte), address payable(增加transfer和send,balance, code, codehash用于ETH转移), bytes32, bytes1, bytes8, bytes, enum, string
function <function name>([parameter types[, ...]]) {internal|external|public|private} [pure|view|payable] [virtual|override] [<modifiers>]
[returns (<return types>)]{ <function body> }
- 函数可见性
- public: 内部和外部均可见
- private: 只能从本合约内部访问,继承的合约也不能使用
- external: 只能从合约外部访问(但内部可以通过this.f()来调用, f是函数名)
- interval: 只能从合约内部访问,继承的合约可以用,default
合约中定义的函数需要明确指定可见性,它们没有默认值
public|private|interval也可用于修饰状态变量,public变量会自动生成同名的getter函数,用于查询数值。未标明可见性类型的状态变量,默认未internal
- 权限/功能关键字,pure|view|payable
- payable(可支付的),带着它的函数,运行的时候可以给合约转入ETH,可以读和写链上的状态变量。
如果函数标记为 payable,用户调用时可以附带 ETH,这些 ETH 会存入合约地址(address(this).balance 会增加)。 - pure 该关键字的函数不改写链上状态,用户直接掉应用它们不需要付gas,不能读或写链上的状态变量
- view 该关键字的函数不改写链上状态,用户直接掉应用它们不需要付gas,可以读但不能写链上的状态变量
<modifiers>: 自定义的修饰器,可以有0个或多个修饰器[returns ()]: 函数返回的变量类型和名称- 在以太坊中,以下语句被视为修改链上状态:
-
- 写入状态变量
-
- 释放时间
-
- 创建其他合约
-
- 使用
selfdestruct
- 使用
-
- 通过调用发送以太币
-
- 调用任何未标记
view和pure的函数
- 调用任何未标记
-
- 使用低级调用(low-level calls)
-
- 使用包含某些操作码的内联汇编
pragma solidity >=0.8.18 <0.9.0- 解构式赋值
function returnNamed() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array) {
// 如果写_number - 2会提示运行revert错误
_number = 2;
_bool = false;
_array = [uint256(3), 2, 1];
}
function readReturn() public pure {
uint256 _number;
bool _bool;
uint256[3] memory _array;
(_number, _bool, _array) = returnNamed();
(, _bool2, ) = returnNamed();
}
- array和struct是引用类型,由于这些变量比较复杂,占用空间大,使用时必须声明数据存储位置.
- data localtion,不同存储位置的
gas成本不同。整体消耗gas从多到少为storage>memory>calldata
- storage 存储在链上,类似计算机硬盘,消耗
gas多,合约里的状态变量默认都是storage - memory 临时存在内存里,不上链,返回数据是变长的情况下,必须加memory修饰,例如:
string,bytes,array和自定义结构 - calldata 临时存在内存里,和memory类似,不同点是不能修改
immutable,一般用于函数参数
- 数据位置和赋值规则
在不同存储类型相互赋值时候,有时会产生独立的副本,有时会产生引用。
storage赋值给本地函数storage,会创建引用,改变新变量会影响原变量memory赋值给memory,会创建引用,改变新变量会影响原变量
- 变量的作用域
- state variable 状态变量,也叫合约变量
- local variable 局部变量,在函数里定义
- global variable 全局变量,通过预留关键字访问,如:
blockhash(uint blockNumber): (bytes32) 给定区块的哈希值 – 只适用于最近的256个区块, 不包含当前区块。
block.coinbase: (address payable) 当前区块矿工的地址
block.gaslimit: (uint) 当前区块的gaslimit
block.number: (uint) 当前区块的number
block.timestamp: (uint) 当前区块的时间戳,为unix纪元以来的秒
gasleft(): (uint256) 剩余 gas
msg.data: (bytes calldata) 完整call data
msg.sender: (address payable) 消息发送者 (当前 caller)
msg.sig: (bytes4) calldata的前四个字节 (function identifier)
msg.value: (uint) 当前交易发送的 wei 值
block.blobbasefee: (uint) 当前区块的blob基础费用。这是Cancun升级新增的全局变量。
blobhash(uint index): (bytes32) 返回跟当前交易关联的第 index 个blob的版本化哈希(第一个字节为版本号,当前为0x01,后面接KZG承诺的SHA256哈希的最后31个字节)。若当前交易不包含blob,则返回空字节。这是Cancun升级新增的全局变量
- 以太单位,
wei,gwei,ehter
assert(1 wei = 1e0)
assert(1 gwei = 1e9)
assert(1 ether = 1e18)
- 时间单位,
seconds,minutes,hours,days,weeks bytes比bytes1省gas- 数据的创建
-
- 对于
memory修饰的动态数组,可以用new操作符来创建,但是必须声明长度,声明后长度不能改变
- 对于
uint[] memory array8 = new uint[](5);
bytes memory array9 = new bytes(9);
uint256[3] memory _array = [uint256(1), 2, 3]; // 静态数组赋值
-
- 动态数组赋值只能先分配空间再一个一个赋值
- 数组成员
- length
- push()
- push(x)
- pop()
mapping的存储位置必须时storage,因此可以用于合约的状态变量,函数中的storage变量和library函数的参数。不能用于public函数的参数或返回结果中。如果映射声明为public,那么Solidity会自动给你创建一个getter函数,可以通过Key来查询对应的Value。- 映射不存储任何键
Key的资讯。真的????? - 对应映射使用
keccak256(h(key) . slot)计算存取value的值 delete操作符会让变量变为初始值constant和immutable状态声明变量后,不能在初始化后更改数值。这样做可以提升合约安全性和节省gas。只有数值变量可以声明constant和immutable,string和bytes可以声明为constant,但不能为immutable
constant变量必须在声明的时候初始化,之后再也不能改变。immutable可以在声明或/构造函数/中初始化
- 修饰器
modifier是Solidity的特殊语法,类似面向对象的decorator,声明函数拥有的特性,并减少代码冗余。
modifier onlyOwner {
require(msg.sender == owner);
_;
}
function changeOwner(address _newOwner) external onlyOwner {
owner = _nerOwner;
}
- 继承的属性和方法
virtual:父合约中的函数,如果希望子合约重写,需要加上virtual关键字override: 子合约重写了父合约中的函数,需要加上override关键字- 注意:用
override修饰public变量,会重写与变量同名的getter函数
- 继承写法
- 继承时要按辈分最高到低的顺序排序
- 如果一个函数在/多个继承/的合约里都存在,必须重写,不要会报错
- 重写在多个父合约中都重名的函数时,
override关键字后面要加上所有父合约名字,例如override(Yeye, Baba)
Solidity中修饰器modifier同样可以继承,用法与函数继承类似,在相应的地方加virtual和override关键字即可- 构造函数的继承
abstract contract A {
uint public a;
constructor(uint _a) {
a = _a;
}
}
- 在继承时声明父构造函数的参数,例如
contract B is A(1) - 在子合约的构造函数中声明构造函数的参数,例如
contract C is A {
constructor(uint _c) A(_c * _c) {}
}
- 子合约调用父合约的函数有直接调用和super调用两种方式
- 直接调用:子合约可以直接用
父合约名.函数名()方式调用父合约函数,例如Yeye.pop() super关键字: 子合约可以利用super.函数名()来调用/最近的父合约函数/。Solidity继承关系按声明时从右到左的顺序
- 钻石继承:在多重+钻石继承链条上使用
super关键字时,super会调用继承链条上的每一个合约相关函数,而不是只调用最近的父合约,但是钻石继承的重复父节点的方法只会调用一次。 //// 这句话例子似乎有问题 - 抽象合约
abstract和接口interface
- 一个智能合约至少有一个未实现的函数,该合约必须标为
abstract,不然编译会报错,另外未实现的函数需要加virtual,以便子合约重写 - 接口
interface类似于抽象合约,但它不实现任何功能。接口的规则:-
- 不能包含状态变量
-
- 不能包含构造函数
-
- 不能继承除接口外的其他合约
-
- 所有函数都必须是external且不能有函数体
-
- 继承接口的非抽象合约必须实现接口定义的所有功能
-
- 如果智能合约实现了某种接口,其他Dapps和智能合约就知道如何与它交互。因为接口提供了两个重要的信息:
-
- 合约里每个函数的
bytes4选择器,以及函数签名函数名(每个参数类型)
- 合约里每个函数的
-
- 接口id
- 另外接口与合约
ABI等价,可以相互转换:编译接口可以得到合约的ABI,利用abi-to-sol工具,也可以将ABI json文件转换未接口sol文件。 - 如果我们知道一个合约实现了
IERA721接口,不需要知道它具体代码实现,就可以与它交互。
IERC721 BAYC = IERC721(/*合约地址*/)
- 三种抛出异常的方法:
error,require和assert,gas消耗不同
error,0.8.4版本后增加,gas少,可以在contract之外定义异常,可以带参数,error必须搭配revert使用
error TransferNotOwner(address sender);
function transferOwner(uint256 tokenId, address newOwner) public {
if(_owners[tokenId] != msg.sender) {
revert TransferNotOwner();
}
_owners[tokenId] = newOwner;
}
require,0.8版本增加,gas消耗随着异常的字符串长度增加,比error命令要高。require(检查条件, "异常的描述")assert,一般用于断言,assert(检查条件),条件不成立的时候,抛出异常

浙公网安备 33010602011771号