# 2022/06/10 Solidity_Day_No3
### 合约结构
- 状态变量
- 函数
- 函数修饰器(`modifier`)
- 事件(`Event`)
- 错误(`Error`)
- 结构体
- 枚举类型
**什么是合约?**
`Solidity`合约类似于面向对象语言中的类
**合约的一些特点:**
- 调用另一个合约实例的函数会执行一个`EVM`函数调用,这个操作会切换执行时的上下文,前一个合约的状态变量就不能访问了
- 构造函数只允许有一个,不支持构造函数重载
- 合约的最终代码将部署到区块链上.代码包括所有公共和外部函数以及所有可以通过函数调用访问的函数.部署的代码没有包括构造函数代码或构造函数调用的内部函数
- 一个合约想要创建另一个合约,那么创建者必须知晓被创建合约的源代码(和二进制代码)
**状态变量等可见性和`getter`函数:**
**特点:**
1. 编译器自动为所有`public`状态变量创建`getter`函数,可以通过调用函数的方式访问到`public`的状态变量
2. `getter`函数在内部访问,是一个状态变量,外部访问则被认为是一个函数
- 状态变量可见性:
- `public` ---> `public`的状态变量会自动生成`getter`函数,可供其他合约读取其值,当状态变量使用外部方式访问,会调用`getter`函数.内部方式则直接从存储中获取值,不可被修改
- `internal` ---> 内部可见性状态变量只能在它们所定义的合约和派生合同中访问
- `private` ---> 派生合约中也不可见
- 函数可见性:
- `external` ---> 作为合约接口的一部分,可从其他合约和交易中调用,**不能从内部调用外部函数**
- `public` ---> 合约接口的一部分,可通过内部或者消息调用
- `internal` ---> 不可以外部访问,可以接受内部可见性类型的参数:比如映射或存储引用
- `private` ---> 派生合约中也不可见
**注意:**
1. `private`或者`internal`声明的状态变量或者函数仍然可以在链外查看,只是不可调用**
2. 对于数组类型的`public`状态变量,自动生成的`getter`函数只可以访问数组的单个元素,需要返回整个数组则需要单独声明一个函数
**示例代码:**
`
contract TestArray {
// Declare an array -> public state variable
uint[] public testArray;
// 声明返回整个数组
function getTestArray() public view returns(uint[] memory) {
return testArray;
}
// 指定生成的getter函数
/*
function testArray(uint i) public view returns(uint) {
return testArray[i];
}
*/
}
`
**对于一些结构体类型的`map`则无法对应每一个结构体属性生成一个`key`**
`
contract Complex {
// Declare a struct
struct Data {
uint a;
bytes3 b;
mapping (uint => uint) map;
uint[3] c;
uint[] d;
bytes e;
}
// Declare a map
mapping (uint => mapping(bool => Data[])) public data;
}
`
生成的`getter`函数是:
`
function data(uint arg1, bool arg2, uint arg3)
public
returns (uint a, bytes3 b, bytes memory e)
{
a = data[arg1][arg2][arg3].a;
b = data[arg1][arg2][arg3].b;
e = data[arg1][arg2][arg3].e;
}
`
#### 函数修改器
**特点:**
1. 函数执行前制动检查条件
2. 修改器是合约的可继承属性,可被派生合约覆盖 ---> 需要被重写的修改器要使用`virtual`修饰
3. 想访问定义在合约`C`当中的修饰器`m`可以用`C.m`进行访问
4. 智能使用当前合约或者所继承基类合约的修饰器,**定义在库当中的修饰器只能被库函数使用**
5. 修改器不能隐式地访问或改变它们所修饰的函数的参数和返回值.这些值只能在调用时明确地以参数传递
6. 修饰器所修饰的函数的`return`语句仅跳出当前修改器和函数,但是执行逻辑会继续往下执行
**示例代码:**
`
contract Mutex {
bool locked;
// Declare a modifier
modifier noReentrancy() {
require(!locked, "Reentrant call.");
locked = true;
_;
locked = false;
}
// Declare a function which is decorated by noReentrancy
function f() public noReentrancy returns (uint) {
(bool success,) = msg.sender.call("");
require(success);
return 7;
}
// 函数受互斥量保护,`msg.sender.call`中重入调用不能再次调用`f`
// `return 7` 指定返回值为7,但是最终的执行结果会以它的修饰器的结果为准 ---> locked = false仍然会被执行
}
`
7. `_`符号可以在修改器中出现多次,每处都会替换为函数体.
#### Constant和Immutable状态变量
- `Constant`(常量) ---> 值在编译时确定,赋值给它的表达式将复制到所有访问该常量的位置,每次都会对其进行重新求值
- `Immutable`(不可变量) ---> 值在部署时确定,只进行一次求值,保留较少的字节存储这些值
**特点:**
只有字符串和值类型支持声明成常量和不可变量
##### constant
**特点:**
1. 状态变量声明为常量,只能使用编译时有确定值的表达式来赋值,任何通过访问`storage`、区块链数据或执行数据或对外部合约的调用给它们赋值是不允许的
2. 内建(`built-in`)函数`keccak256`、`sha256`、`ripemd160`、`ecrecover`、`addmod`、`mulmod`是允许的(即便这些函数会调用外部合约)
##### immutable
**特点:**
1. 不可变量只能赋值一次,并且在赋值之后才可以读取.
2. 不可变变量可以在声明时赋值,但是只有在合约的构造函数执行时才被视为初始化 ---> 建议在合约构造函数中对不可变变量赋值,这样防止继承的时候初始化顺序出现问题导致不可变变量不可访问
#### 函数
**分类:**
> - 视图函数
> - 纯函数
> - 特别函数
> - 回退函数
##### 内部函数和外部函数
- 自由函数(定义在合约之外的函数),具有隐式`internal`可见性,类似内部库函数
**自由函数的特点:**
- 可以在合约内部调用自由函数
- 编译器会将自由函数的代码添加到合约中
**自由函数与定义在合约内部的函数的区别:**
***自由函数不能直接访问存储变量和不在他们的作用域范围内函数***
- `Solidity`和`Go`类似,支持多个返回值,多个返回值返回之前需要先声明出来,使用`returns`进行返回值声明
**示例代码:**
`
pragma solidity ^0.8.0;
contract MoreReturns {
// 显示的在多个返回值之前声明返回值并赋值
function arithmetic(uint a, uint b) public pure returns (uint sum, uint product) {
sum = a + b;
product = a * b;
}
// 显示的在多个返回值之前声明不进行赋值操作
function arithmeticNo2(uint a, uint b) public pure returns (uint sum, uint product) {
return (a + b, a * b);
}
}
`
##### 视图函数
将函数声明为`view`类型,声明以后要不修改状态
**被认为是修改状态的语句:**
1. 修改状态变量
2. 产生事件 ---> `Events`
3. 创建其他合约 ---> 使用语句`new`
4. 使用`selfdestruct` ---> 自毁
5. 调用发送以太币
6. 调用任何没有标记为`view`或者`pure`的函数
7. 使用低级调用
8. 使用包含特定操作码的内联汇编
**示例代码:**
`
pragma solidity ^0.8.0;
contract FunctionView {
function f(uint a, uint b) public view returns (uint) {
return a * (b + 42) + block.timestamp;
}
}
`
**注意:**
所有的`getter`函数都会被自动标记为`view`
##### 纯函数
**特点:**
声明为`pure`的函数不读取也不修改状态变量
**被认为是读取状态:**
1. 读取状态变量
2. 访问`address(this).balance`或者`<address>.balance`
3. 访问`block`,`tx`,`msg`中任意成员(不包括`msg.sig`和`msg.data`)
4. 调用未标记为`pure`的函数
5. 使用包含某些操作码的内联汇编
**示例代码:**
·
pragma solidity ^0.8.0;
contract FunctionPure {
function f(uint a, uint b) public pure returns (uint) {
return a * (b + 42);
}
}
·
**注意:**
- 纯函数可以使用`revert`和`require`在发生错误时还原潜在的状态更改
- 还原状态更改不被视为状态修改
- 不可能在`Evm`级别阻止函数读取,只能阻止函数写入
##### 特别函数
`receive`接收以太函数,声明形式:
- `receive() external payable { ... }`
**特点:**
- 不需要`function`关键字,也没有参数和返回值并且必须是`external`可见性和`payable`修饰.可以是`virtual`的,可以被重载也可以有修改器`modifier`
- 对合约进行转账会调用`receive`函数,如果`receive`函数不存在,但是有`payable`的`fallback`回退函数,那么在转账时会调用`fallback`函数,如果两个函数都没有就会`panic`
- 没有`receive`函数的合约可以作为`coinbase`交易的接收者或者作为`selfdestruct`的目标来接收以太币,合约不能对这种以太币转移做出反应,因此也不能拒绝它们.所以`address(this).balance`的指可以高于合约中实现的一些手工记账的总和
会消耗2300gas的操作:
- 写入存储
- 创建合约
- 调用消耗大量`gas`的外部函数
- 发送以太币
**示例代码:**
`
pragma solidity ^0.8.0;
contract TestTransfer {
// 保留所以发送给该可约的以太币并且没法取出
event Received(address, uint);
receive() external payable {
emit Received(msg.sender, msg.value);
}
}
`
##### Fallback和Receive
`fallback`函数的声明形式:
- `fallback () external [payable]`
- `fallback (bytes calldata input) external [payable] returns (bytes memory output)`
**特点:**
1. 不需要`function`关键字,也没有参数和返回值并且必须是`external`可见性和`payable`修饰.可以是`virtual`的,可以被重载也可以有修改器`modifier`
2. `fallback`函数始终会接收数据,为同时接收以太,必须标记为`payable`
**注意:**
- 要解码输入数据,前四个字节用作函数选择器,然后用`abi.decode`与数组切片语法一起使用来解码ABI编码的数据 ---> `(c, d) = abi.decode(_input[4:], (uint256, uint256));`
**示例代码:**
`
pragma solidity ^0.8.0;
import "./TestTransfer.sol";
contract TestTransferFlow {
// 定义一个异常的函数
uint x;
fallback() external {x = 1;}
}
// 该合约保留所有发送给该合约的以太币,无法退还
contract TestPayable {
uint x;
uint y;
// 任何对合约非空calldata调用会执行回退函数(即使是调用函数附加以太)
fallback() external payable {x = 1;
y = msg.value;}
// 纯转账调用这个函数,例如对每个空empty calldata的调用
receive() external payable {x = 2;
y = msg.value;}
}
contract Caller {
function callTest(TestTransfer testTransfer) public {
(bool success,) = address.call(abi.encodeWithSignature("nonExistingFunction()"));
require(success);
// 这个操作会修改上方的TestTransfer函数的状态变量为1,因为已经通过abi的形式call过了一遍合约里的函数.
// 在合约当中一个完整的转账是一个事务,如果其中任何一个缓解失败了那么应该是要可以回滚的.所以合约把事务定义成了一个类型address payable
address payable testPayAble = payable(address(testTransfer));
// 这个操作会做下面的操作,但是下面的操作不会被编译
// test.send(2, ether);
}
function callTestPayAble(TestPayable testPayAble) public returns (bool) {
(bool success,) = address(testPayAble).call(abi.encodeWithSignature("nonExistingFunction()"));
require(success);
// 这个操作会调用fallback函数 -> x = 1, y = 0
// 注意:.call是一个函数
(success,) = address(testPayAble).call{value : 1}(abi.encodeWithSignature("nonExistingFunction()"));
require(success);
// 这样调用x = 1, y = 1, 调用的是fallback函数
(success,) = address(testPayAble).call{value : 2 ether}("");
require(success);
// 这样调用了receive函数,x = 2, y = 2 ether; ---> 只有发送以太币的函数才会调用receive函数
return true;
}
}
`
##### 函数重载
**注意:**
1. 重载函数存在于外部接口当中,如果重载函数区别仅在于`solidity`内的类型而不是外部类型则会导致错误
**示例代码:**
`
pragma solidity ^0.8.0;
contract OverLoadFunction {
// 声明两个重载函数
function f(B value) public pure returns (B out) {
out = value;
}
function f(address value) public pure returns (address out) {
out = value;
}
// 由于B是内置类型在abi当中被认为是address类型,所以上面的代码无法编译
}
contract B {}
`
2. 注意重载函数中的参数类型长度位
**示例代码:**
`
contract A {
function f(uint8 value) public pure returns (uint8 out) {
out = value;
}
function f(uint256 value) public pure returns (uint256 out) {
out = value;
}
// 如果发f(50)则会报错,因为50可以被隐式转换成uint8也可以被隐式转换为uint256
}
`
##### 事件Events
**定义:**
`EVM`日志功能的抽象
**调用、监听方式:**
应用程序通过以太坊客户端的`RPC`接口订阅和监听这些事件
**特点:**
1. 事件可以被继承,事件被触发时参数会被存储到交易的日志中
2. 事件可以根据`topic`主题进行搜索,可以过滤掉不需要的合约地址的事件
**示例代码:**
`
var options = {
fromBlock: 0,
address: web3.eth.defaultAccount,
topics: [ADDRESS.ZEROADDRESS, null, null]
};
web3.eth.subscribe("log", options, function (error, result) {
if (!error) {
console.log(result);
}
}).on("data", function (log) {
console.log(log);
}).on("changed", function (log) {
console.log(log);
});
});
`
3. 如果没用`anonymous`来声明事件,那么事件签名的哈希值是一个`topic`,事件过滤仅能通过合约地址进行过滤
**通过函数触发事件可以避免直接对事件进行调用:**
`
pragma solidity ^0.8.0;
contract EmitEvents {
// Declare a event
event Deposit(
address indexed from,
address indexed id,
uint value
);
function deposit(bytes32 id) public payable {
emit Deposit(msg.sender, id, msg.value);
}
// indexed是叫topic的数据结构<abi_events>
// 正常情况下topic仅有32bytes,如果引用类型被标记为indexed, 则该引用类型的keccak-256 hash值会被作为topic保存
// 所有非indexed参数是`abi_encoded`将存储在日志的data部分中
// 通过topic可以搜索事件
}
`
**通过js调用监听事件:**
`
import LockerFirstAbi from "../test/MockAbi/LockerFirstAbi.json";
var abi = LockerFirstAbi; /** abi由编译产生 */
var ClientReceipt = web3.eth.contract(abi);
var clientReceiptObject = ClientReceipt.at("传入部署好的合约地址");
// 声明事件变量
var depositEvent = clientReceiptObject.Deposit();
// 开始监听变化
depositEvent.watch(function (error, result) {
// 结果包含 indexed 以及 topic
if (!error) {
console.log(result);
}
});
// 通过回调函数进行监听
var depositEvent = clientReceiptObject.Deposit(function (error, result) {
if (!error) {
console.log(result);
}
});
`
##### 错误和回退语句
`solidity`中的`error`可以被定义在合约(包括接口和库)内部和外部
**特点:**
1. 错误必须与`revert`语句一起使用,像事务回滚一样还原发生的变化并返回错误信息
2. 错误不能被重写或覆盖,但是可以继承. ---> 只要作用域不同,同一个错误可以在多个地方定义.只能使用`revert`创建错误实例
3. 只有外部调用的错误才能被捕获,内部调用或者同一函数内的`revert`不能被捕获
4. 如果错误没有任何参数,错误只需要四个字节的数据.可以使用`NatSpec`来解释错误
##### 继承机制
**`solidity`的继承机制像`c++`可以多继承,继承系统上与`python`类似**
**特点:**
1. 最终的派生合约都会被调用
2. 当一个合约从多个合约继承时,在区块链上只有一个合约被创建,所有基类合约的代码被编译到创建的合约中 ---> super.f(..)将使用`JUMP`跳转而不是消息调用
3. 子合约不可以在声明已经是基类合约
**示例代码:**
`
pragma solidity ^0.8.0;
contract MultipleInheritance {
// 声明一个构造器
constructor() public {owner = payable(msg.sender);}
address payable owner;
}
// 使用is从另一个合约派生,子合约可以访问所有非私有成员,包括内部internal函数和状态变量.
// 无法通过this来外部访问
contract Destructible is MultipleInheritance {
// virtual表示该函数可以在子合约当中重写
// 声明一个销毁合约的函数 ---> 函数中的owner是父合约当中的状态变量
function destroy() virtual public {
if (msg.sender == owner) selfdestruct(owner);
}
}
// 这些抽象合约仅用于给编译器提供接口.函数没有函数体。
// 如果一个合约没有实现所有函数,则只能用作接口.
abstract contract Config {
function lookup(uint id) public virtual returns (address adr);
}
abstract contract NameReg {
function register(bytes32 name) public virtual;
function unregister() public virtual;
}
contract Named is MultipleInheritance, Destructible {
// 可以多重继承,但是MultipleInheritance也是Destructible的父类,所以只会有一个MultipleInheritance实例.类似(c++)当中的多重继承
constructor(bytes32 name) {
Config config = Config("抽象合约的hash地址");
// 下面调用的是抽象合约的函数,因为config传入的是抽象合约的地址所以config可以调用.lookup函数,又因为转型成了NameReg合约类型所以可以调用register函数
NameReg(config.lookup(1)).register(name);
}
// 函数可以被另一个具有相同名称和相同数量/类型输入的函数重载 ---> 如果重载函数有不同类型的输出参数,会导致错误
// 本地和基于消息的函数调用都会考虑这些重载
// A函数重写了B函数,A函数本身又可以被重写,如果A函数需要被重写则需要声明为virtual
function destory() public virtual override {
if (msg.sender == owner) {
Config config = Config("抽象合约的hash地址");
NameReg(config.lookup(1)).unregister();
// 重写了父合约函数的函数仍然可以调用父合约函数的被重写函数
Destructible.destroy();
}
}
}
// 如果父合约的构造器接收函数那么需要在继承的时候给父合约传入构造器需要的参数,或者在子合约的构造器位置用修改器的调用风格提供父合约的构造参数
contract PriceFeed is MultipleInheritance, Destructible, NameReg("GoldFeed") {
uint info;
function updateInfo(uint newInfo) public {
// 这里的info是状态变量
if (msg.sender == owner) info = newInfo;
}
// 由于在上方的多继承并且多继承当中存在多个被重写的函数,在这里希望指定使用其中的任意一个
// 所以在这里将新重写的destroy函数声明为不可再被重写
function destroy() public override(Destructible, Named) {
// 这里的如参的意思是可以传入上诉两个合约的类型参数
// 传入了以后只会指定的调用Named合约的destroy函数
Named.destroy();
}
function get() public view returns (uint r) {
return info;
}
}
`
**上诉代码当中`Named`合约调用了`Destructible`的`destory()`函数,这样调用的结果是会存在如下的问题:**
`
pragma solidity ^0.8.0;
contract ErrorContract {
constructor() {
owner = payable(msg.sender);
}
address owner;
}
contract Destructible is ErrorContract {
function destroy() public virtual {
if (msg.sender == owner) {
// 自我销毁
selfdestruct(owner);
}
}
}
contract BaseNo1 is Destructible {
// 重写基类合约的销毁函数 -> 重写为可重写并已重写状态
function destroy() public virtual override {
// 调用基类合约的基类合约中的销毁函数
Destructible.destroy();
}
}
contract BaseNo2 is Destructible {
function destroy() public virtual override {
// 调用基类合约的基类合约中的销毁函数
Destructible.destroy();
}
}
// 声明一个合约继承了BaseNo1和BaseNo2,在调用BaseNo2的销毁函数
contract Final is BaseNo1, BaseNo2 {
// 显示声明重写了那些基类合约的函数
function destroy() public override(BaseNo1, BaseNo2) {
BaseNo2.destroy();
}
// 这个问题是在于重写的时候显示的制定了重写BaseNo1和BaseNo2合约当中的destroy函数,但是在Final合约当中显示指定了BaseNo2的destroy函数
// 所以最终如果调用Final合约当中的destroy函数会绕过BaseNo1当中的destroy函数
}
`
**解决方式:**
`
pragma solidity ^0.8.0;
contract SuperMultiple {
// 如果想再多继承当中不会绕过其中一个基类的同名函数,那么应该使用关键字`super`
constructor() {
owner = payable(msg.sender);
}
address owner;
}
contract Destructible is SuperMultiple {
function destroy() public virtual {
if (msg.sender == owner) {
// 自我销毁
selfdestruct(owner);
}
}
}
contract BaseNo1 is Destructible {
// 重写基类合约的销毁函数 -> 重写为可重写并已重写状态
function destroy() public virtual override {
// 调用基类合约的基类合约中的销毁函数
super.destroy();
}
}
contract BaseNo2 is Destructible {
function destroy() public virtual override {
// 调用基类合约的基类合约中的销毁函数
super.destroy();
}
}
// 声明一个合约继承了BaseNo1和BaseNo2,在调用BaseNo2的销毁函数
contract Final is BaseNo1, BaseNo2 {
// 显示声明重写了那些基类合约的函数
function destroy() public override(BaseNo1, BaseNo2) {
super.destroy();
}
}
`
**使用`super`在当前类中调用的实际函数在当前类的上下文未知,类型已知**
##### 函数重写(`Overriding`)
**可被重写的特点:**
父合约标记为`virtual`函数可以在继承合约里重写(`overridden`).重写的函数需要使用关键字`override`修饰.如果已经重写过的函数还需要支持可在被重写,可以同时用`virtual`继续修饰
**重写函数可见性的改变:**
1. 只能覆盖函数将可见性从`external` -> `public`
2. 可变性的变化: `nonpayable`可被`view`和`pure`覆盖.`view`可被`pure`覆盖.`payable`不可更改为其他可变性
**示例代码:**
`
pragma solidity ^0.8.0;
contract Overriden {
// 在一个函数当中声明了其的可见性(external)和可变性(view)和可重写性(virtual)
function foo() virtual external view {}
}
contract Middle is Overriden {
// 继承基类合约
}
contract Inherited is Middle {
// 重写基类合约当中的基类合约里面定义的函数
// 修改其可见性、可变性、可重写性
function foo() override public pure {}
}
`
**采用多继承的方式如果多个基类合约有相同定义的函数那么`override`关键字后必须指定所有基类合约名:**
`
pragma solidity ^0.8.0;
contract ErrorContract {
constructor() {
owner = payable(msg.sender);
}
address owner;
}
contract Destructible is ErrorContract {
function destroy() public virtual {
if (msg.sender == owner) {
// 自我销毁
selfdestruct(owner);
}
}
}
contract BaseNo1 is Destructible {
// 重写基类合约的销毁函数 -> 重写为可重写并已重写状态
function destroy() public virtual override {
// 调用基类合约的基类合约中的销毁函数
Destructible.destroy();
}
}
contract BaseNo2 is Destructible {
function destroy() public virtual override {
// 调用基类合约的基类合约中的销毁函数
Destructible.destroy();
}
}
// 声明一个合约继承了BaseNo1和BaseNo2,在调用BaseNo2的销毁函数
contract Final is BaseNo1, BaseNo2 {
// 显示声明重写了那些基类合约的函数
function destroy() public override(BaseNo1, BaseNo2) {
BaseNo2.destroy();
}
// 这个问题是在于重写的时候显示的制定了重写BaseNo1和BaseNo2合约当中的destroy函数,但是在Final合约当中显示指定了BaseNo2的destroy函数
// 所以最终如果调用Final合约当中的destroy函数会绕过BaseNo1当中的destroy函数
}
`
**如果函数继承自一个公共基类合约,那么`override`不需要显示声明:**
`
pragma solidity ^0.8.0;
contract ImplicitDeclaration {
function ImplicitDeclaration() public pure {}
}
contract B is ImplicitDeclaration {}
contract C is ImplicitDeclaration {}
// 此时声明一个D继承B和C即可重写ImplicitDeclaration当中的函数,无需显示声明override
contract D is B, C {}
`
**可被重写函数的特点:**
1. `private`函数不可被标记为`virtual`
2. 除接口之外,没有实现的函数必须标记为`virtual`
3. 重写接口函数时不再要求`override`关键字,除非函数在多个父合约定义.