---
# System prepended metadata

title: Solidity基础
tags: [以太坊基础]

---

# pragma

pragma与许可证明
```solidity
// SPDX-License-Identifier: GPL-3.0
// 如果你的合约要发布到链上，那么上面的许可非常重要
// 它是一个机器可读的许可
pragma solidity >=0.4.16 <0.9.0;

contract SimpleStorage {
    uint storedData;

    function set(uint x) public {
        storedData = x;
    }

    function get() public view returns (uint) {
        return storedData;
    }
}
// pragma版本：上述版本pragma solidity >=0.4.16 <0.9.0;指定该合约使用的solidity语言版本必须大于等于0.4.16，同时小于0.9.0。

// 注意：pragma solidity ^0.5.2指定该合约使用的solidity语言版本需要大于等于0.5.2，但是必须小于0.6.0。（这一切归因于^）

```

#区块链

## 区块链概念

区块链是一个全球共享的数据库，每个区块链网络中的节点都可以对其进行读取。如果需要对数据库进行修改，需要发送交易来进行尝试。所谓尝试是说：这个交易必须被区块链网络中的其他节点所接收（共识）才有可能更改数据库的内容；如果交易的内容不合法（同时修改两个值），那么无法通过共识，因此无法修改（交易回滚）。
## 区块
区块是交易的集合，如下图：世界状态在经过一个区块后，从t1态更新为t2态。
![image](https://hackmd.io/_uploads/BJYpfw4sye.png)

权益证明（POS）的基本步骤：
1. 验证者向以太坊质押32个eth，用以遏制不良行为。
2. 每个时隙（12秒，slot，每个32个slot称为一个时段，epoch）会随机选择一个验证者作为区块提议者，将交易打包并执行以确定一个新的世界状态。而后，它将一系列信息包装到区块中，传送给其他验证者。
3. 其他验证者验证该区块的有效性，如果同意接收该区块，则将区块添加到自身数据库中。
4. 如果验证者检查到一个时隙中存在两个冲突区块（区块提议者发送两个区块到网络中，由于p2p网络的特性，不同的验证者会接收到不同的区块），它们可以用自己的分叉选择算法选择获得质押最多的那个区块。
需要注意的是：以太坊的权益证明会在每个时段的第一个区块（称为检查点）进行Justified和Finalized。如果检查点的区块Finalized，则该区块的所有祖先区块都会被Finalized。如果一个区块没有被Finalized，那么它还有可能回滚。这个过程如下：
一个区块被提议后，会广播给其他验证者，验证者验证通过后投prepare票，如果收集到2/3的prepare票（基于质押者数量），那么进入Justified状态。当区块Justified之后，验证者再次投commit票，如果收集到2/3的commit票，该区块将被Finalized。

## 区块示例

区块示例：https://beaconscan.com/slot/10033886
![image](https://hackmd.io/_uploads/BJV1XvEsyl.png)
![image](https://hackmd.io/_uploads/HJv1XwVsyl.png)

## 双花攻击

如果有一个账户的余额只有1ether，它同时向其他两个账户发送了1ether，也即将它的余额花费两次，这就是双花攻击。
在区块链领域中，上述行为需要同时发送两笔交易；而后这两笔交易在区块中会存在执行的先后顺序，如果交易1先执行，那么账户的余额已经为空，交易2就会被回滚。这里的关键点在于区块中已经为交易排序（这个排序经过了共识），因此如果要造成双花攻击，需要操纵共识机制。（2/3的验证者都是你的人！）


## EVM
概述
EVM是智能合约运行环境，它与外部网络环境完全隔离。

账户
EVM中账户分为EOA账户和合约账户，这些账户都是由私钥控制的。EOA账户的地址由公钥进行keccak256生成，而合约账户的地址是由下面的公式计算：
`keccak256(rlp_encode(creator_account, creator_account_nonce))[12:]`
将合约创建者的地址和nonce进行rlp编码，得到的结果进行keccak256计算，最后截断尾部20字节。

一个账户拥有四个字段：
1. nonce：计数器，记录了当前账户发送交易的数量。
2. code_hash：字节码哈希，如果是EOA账户此字段为空。
3. balance：余额。
4. storage：slot与合约状态变量的映射，如果是EOA账户此字段为空。
关于nonce：

如果是EOA账户，每发送一笔交易其nonce++；如果是合约账户，每创建一个合约其nonce++。

nonce的作用之一是交易排序：如果你同时发送了两笔交易给矿工，矿工可根据你的nonce为交易排序。nonce的另一作用是防止重放攻击：一笔交易的nonce值代表了交易发起人发送交易的数量。如果一笔交易不包含nonce值，那么任何人都可以复制该交易重复执行。如果一笔交易包含nonce值，那么针对该交易发起者，其同一nonce值的交易只能被打包一次。

## 交易
一笔交易本质上是一个账户发送给另一个账户的消息。
```
    EOA账户 -> 合约账户：调用合约中的函数。
    合约账户 -> 合约账户：称为内部交易，属于合约之间的调用。
    合约账户 -> EOA账户：可能是给EOA账户转账
    EOA账户 -> EOA账户：转账。
```
如果交易的to地址为空，则说明此交易在创建合约。这种合约创建交易的calldata是合约的creationCode+构造函数参数。注意：在创建合约时（也即执行构造函数时），其字节码为空，因此在不应在构造函数中回调当前合约。

## GAS
每笔交易在创建时都会被收取一定量的 gas，这笔费用必须由交易的发起人（tx.origin）支付。当以太坊虚拟机（EVM）执行交易时，gas 会根据特定规则逐渐耗尽。如果在任何时候 gas 被耗尽（即变为负数），就会触发 gas 耗尽异常，这会结束执行并将当前调用帧对状态所做的所有修改回滚。这种机制鼓励经济地使用 EVM 执行时间，并且也补偿 EVM 执行者（即矿工/质押者）的工作。由于每个区块都有最大的 gas 量，它也限制了验证一个区块所需的工作量。

gas 价格是由交易发起人设定的一个值，发起人必须预先支付 `gas_price * gas` 给 EVM 执行者。如果执行后还有剩余的 gas，将会退还给交易发起人。如果发生异常导致更改被回滚，已经用掉的 gas 是不会退还的。

由于 EVM 执行者可以选择是否包含一笔交易，因此交易发送者不能通过设定一个很低的 gas 价格来滥用系统。

## Stack,Memory,Storage
EVM具有三个可存储数据的空间：Stack、Memory和Storage。其中Stack、Memory是临时存储，而Storage是持久存储。

每个合约都维护自身的Storage，合约无法读取或写入其他合约的Storage（如果Storage修饰为public，可以访问）。

Memory和Stack会在每次消息调用重新创建（在创建前会保存其内容）。

### Memory

Memory就是一个**动态字节数组**，该数组限制读取宽度为32字节，而写入宽度可以是1字节或32字节。当读取或写入未触及的内存地址时，会进行内存扩展（每次扩展的单位时32字节，扩展完成后的内存长度为32字节整数倍）。在进行内存扩展时需要消耗gas。

一个内存扩展的例子
操作码：PUSH1 0xFF PUSH1 0X0 MSTORE PUSH1 0XFF PUSH1 0X22 MSTORE
![image](https://hackmd.io/_uploads/r1MJVP4iJg.png)

### Stack

Stack是一个动态数组，该数组的每个元素长度为32字节，数组的最大长度为1024。EVM是一个堆栈机，一切计算都是在Stack中进行的。由于Stack仅限于访问顶端元素，因此对其他元素的读取依赖于Swap指令或Dup指令。

## 指令集
EVM 的指令集保持最小化，以避免可能导致共识问题的不正确或不一致的实现。

## Message Call=Call
Meesage定义：在两个账户之间传输的数据（from、to、value、data等等），形式是合约的确定性操作或者经过加密安全签名的交易。

Message Call定义：将消息从一个账户传递到另一个账户，如果目标账户的字节码非空，则根据目标账户的账户以及消息的内容执行。且如果发送账户是一个合约账户，那么执行的返回值将立即返回。

由上述内容，可知Message Call就是一笔内部交易，也就是一般理解的Call。

在solidity合约内部可以指定一次Message Call对gas的使用量，如果本次Message Call由于gas耗尽失败，也只会消耗指定用量。Call调用栈的深度为1024，由于每次Message Call只能转发63/64的gas，因此实际上Call调用深度会小于1000。

## Delegate Call
Delegatecall是Message Call的变体，只是借用目标地址的代码在当前的上下文中执行（msg.sender和msg.value都不改变）。这意味着，合约可以在任意上下文动态加载其他合约的代码。

## Logs
Solidity的事件就是根据日志功能来实现的，日志可以将数据存储在特殊索引的数据结构中，每个区块都有一个该结构。合约无法直接访问日志中的数据。日志数据的一部分存储在布隆过滤器中，因此可以高效且加密的方式搜索这些数据，即使是轻客户端也可以找到这些日志内容。

## Create
合约可以直接通过字节码来创建新的合约账户，包括Create和Create2字节码。

## Selfdestruct
从区块链中删除代码的唯一办法是当前合约执行自毁操作，存储在该地址的剩余eth将被发送到指定目标账户，而后该合约账户的Storage和代码都被永久删除。
注意：
1. 在Cancun之后执行自毁操作只会将所有eth转移到指定目标地址，而不会销毁合约。即使合约执行了自毁操作，依然可以被调用。但是，如果在合约创建的同时执行自毁操作，还是可以达到销毁合约的目的。
2. 自毁已经被弃用了，但是未被删除。
3. 销毁合约和从硬盘中删除内容不同，其他的节点有可能依然保存了该合约的历史数据。
4. 自毁仅仅会将剩余eth发送给指定用户，而ERC20代币将永久丢失。
5. 如果指定目标账户不存在，会直接创建一个新的账户。

## 预编译合约

预编译合约是 EVM 中用于提供更复杂库函数 ( 通常用于加密、散列等复杂操作 ) 的一种折衷方法，也可以理解为一种特殊的合约，这些函数不适合编写操作码。 它们适用于简单但经常调用的合约，或逻辑上固定但计算量很大的合约。 预编译合约是在使用节点客户端代码实现的，因为它们不需要 EVM，所以运行速度很快（直接编写在geth代码层面）。 与使用直接在 EVM 中运行的函数相比，它对开发人员来说成本也更低。