`

solidity

 
阅读更多
一.solidity
1.EVM 不是基于寄存器的,而是基于栈的,因此所有的计算都在一个被称为栈(stack)的区域执行,最大有1024个元素,每个元素长度是一个字(256位)
1.1.收到一个区块时,会遍历里面的所有交易,然后将交易进行applyMessage
1.2.取出当前的状态数据库,判断交易是转账还是合约交易
1.3.进到evm后,会先设置一个快照,方便revert后,undo回到快照.
1.4.对合约进行预编译判定,如果有预编译,则由可以执行合约
1.5.如果没有预编译,则由解释器对合约进行解释执行,通过本地的jump_table,取出对应的每一个"脚本"的执行函数进行执行操作
1.6.如果执行成功则将结果和多出的gas返回,否则触发revert,undo回到之前设置的快照
1.7.创建一个收据, 用来存储中间状态的root, 以及交易使用的gas
1.8.拿到所有的日志并创建日志的布隆过滤器
1.9 事件数据存储
以太坊的bloom过滤器大大的提高了查询的效率。以太坊先创建topics的bloom,再创建logs的bloom,再创建收据的bloom,在创建header的bloom,最后创建block的bloom,一步一步构建上去。
1.10 事件查询
即先根据查询条件得到布隆过滤器,先在block的bloom里面找,如果其位向量是区块布隆过滤器的子向量,则认为可能在此区块有,再在header的bloom里面找,再在收据的bloom里面找,直到找到最终的日志。


2.存储:每个账户有一块持久化内存区。将256位字映射到256位字的键值存储区。 在合约中枚举存储是不可能的,且读存储的相对开销很高,修改存储的开销甚至更高。合约只能读写存储区内属于自己的部分。
2.1 内存:合约会试图为每一次消息调用获取一块被重新擦拭干净的内存实例。内存是线性的,可按字节级寻址,但读的长度被限制为256位,而写的长度可以是8位或256位。当访问(无论是读还是写)之前从未访问过的内存字(word)时(无论是偏移到该字内的任何位置),内存将按字进行扩展(每个字是256位)。扩容也将消耗一定的gas。 随着内存使用量的增长,其费用也会增高(以平方级别)。

2.2 EVM:对栈的访问只限于其顶端,限制方式为:允许拷贝最顶端的16个元素中的一个到栈顶,或者是交换栈顶元素和下面16个元素中的一个。所有其他操作都只能取最顶的两个(或一个,或更多,取决于具体的操作)元素,运算后,把结果压入栈顶。当然可以把栈上的元素放到存储或内存中。但是无法只访问栈上指定深度的那个元素,除非先从栈顶移除其他元素

3.数据位置
3.1 memory:合约中的本地内存变量,函数参数,返回的参数默认是memory
3.2 storage:局部变量默认是,状态变量强制是,每32字节20000gas
3.3 calldata:外部函数的参数,与memory类似,是一块只读且不会永久存储的位置,计算需要消耗n*68(n为calldata中非0字节数)的Gas费用
3.4 log:最多增加3个index,这些可以被索引过滤,每字节花费8gas

4.编码函数
4.1 abi.encode()returns(bytes):对给定参数进行编码
4.2 abi.encodePacked()returns(bytes):对给定参数进行紧打包编码,不补0
4.3 abi.encodeWithSelector(bytes4 selector,...) returns(bytes):对给定参数进行编码,并以给定的函数选择器作为起始的4字段数据一起返回
4.4 abi.encodeWithSignature(string signature,...) returns (bytes):等价于abi.encodeWithSelector(bytes4(keccak256(signature),...)
4.5 abi.encodeCall(function functionPointer, (...)) returns (bytes memory):使用 tuple 类型参数 ABI 编码调用 functionPointer()。
执行完整的类型检查,确保类型匹配函数签名。结果和 abi.encodeWithSelector(functionPointer.selector, (...)) 一致。

5.Library
5.1 库和合约的区别在于库不能有Fallback函数以及paynable关键字,不能接收eth,也不能定义storage变量.但是库可以修改和它们相链接的合约的storage变量,类似为一个函数传入一个C的指针
5.2 库不能有日志(Event),但可以分发事件,通过using xxx for xxx,作用于其它合约变量,当触发库event时需要监控其它合约才行
5.3 一个合约的字节码的长度,就不能超过24576字节,Library自己是个单独部署的合约,其中的external函数,供其他合约以delegatecall的方式来调用。这样其他合约就可以减小自己的体积了.
5.4 Library最好只包含internal的函数,这些函数会被复制进你部署的合约中,类似静态链接库,这样可以减少Gas消耗
5.5 库函数接受storage类型的函数时,的确采用了传引用的方式。因此,如果上面的函数声明是function findMin(uint[] storage arr) external returns (uint) ,它就能以较低的Gas代价,实现一个动态链接库函数的效果。
5.6 如果库函数被调用,它的代码在调用智能合约的上下文中被执行,即this指向调用智能合约,特别是可以访问调用智能合约的存储.
5.7 一般可以在库内定义一些结构体,在别的合约中使用,以减少其它合约的大小


6.参数列表
6.1 最多12个变量(栈深超过一定数量则会溢出)
6.2 事件index最多3个,它们与事件签名的keccak哈希值一起组成日志项的topic,未索引的部分就组成事件的字节数组

7.标准修饰符
7.1 internal 类似c++中的protected,通过JUMP指令实现,参数以内存指针传递,消耗gas最少
7.2 external 函数参数直接从calldata读取,内存分配比较便宜
7.3 public 会自动创建getter,通过jump指令调用,会复制函数参数到memory(因为不知道调用者是external还是internal),也这是比external更消耗gas的一个原因
7.4 private 私有,仅当前合约能访问
7.5 constant 被修饰的函数没有能力改变区块链上的状态变量,它们可以读取状态变量返回给调用者
7.6 view:不能修改,类似constant,但如果使用staticcall有可能是可以修改的                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         
7.7 pure:不能读写状态变量
7.8 payable:可以从调用者接受eth,如果没有发送eth则调用会失败

8.fallback
8.1 不能有参数和返回值
8.2 call不存在的函数会触发fallback
8.3 如果要接收转账则需要标记payable,但优先级比receive低
8.4 payable fallback 和 receive 函数最多可消耗 2300 gas,普通 fallback 则无此限制,只要gas足够,任何复杂操作都可以执行。


9.继承
9.1 当一个合约的构造函数为internal时,则合约会被自动标记成抽象合约    

10.接口
10.1 无法继承其他智能合约或接口
10.2 无法定义构造函数
10.3 无法定义变量
10.4 无法定义结构体
10.5 无法定义枚举               


11.异常
在可能发生燃料耗尽异常的情况下,为了防止安全问题,至少1/64的发送者剩余的燃料会被保留.这个设计允许发送者来处理内部调用的燃料耗尽异常错误,
能够来停止代码的执行,不会因为燃料耗尽导致无法停止,而将异常向上传递          

12.abstract:非接口,抽像合约
virtual:需要子类实现的抽像合约中的函数
override:重写父合约的函数

13.transfer和send的区别
transfer:发生错误会抛出异常,只有2300的gaslimit
send:不抛出异常,返回值会为false,如果操作者是合约账户,在fallback中出错,而没抛出异常终止,则有可能会阻塞

14.合约接收金额:构造函数是payable,有fallback或receiver
16.1 区块收益地址是合约地址
16.2 其它合约selfdestruct
16.3 在合约创建前预先计算出地址进行转账

15.uncheck
0.8.0版本开始,默认情况下,算术运算使用的是“checked”模式,会进行溢出检查,如果结果溢出,会出现失败异常回退。
加上unchecked{}则不检查是否溢出,溢出则截断显示

16.hardhat插件
npm i hardhat-tracer 装debug

17.interface:类似其他语言中的 interface,可以:
17.1 继承其他接口
17.2 只有方法声明,无其他
17.3 所有方法均为 external
                                                                                                         


二.OpenZeepelin
ERC165
0.1 IERC165:只有一个函数,用来检查合约的指纹是否和指定的接口的指纹相符
0.2 ERC165Checker:用于查询对通过声明的接口的支持的库
0.3 ERC165Storage:将支持的接口存入map,并可以注册新的接口
0.4 IERC1820Registry:接口注册表,各合约可以在此注册表上注册实现了的接口,在调用某个地址上的函数时可以通过注册表查询是否已注册实现相关接口
IERC1820Registry constant internal _ERC1820_REGISTRY = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);
_erc1820.setInterfaceImplementer(address(this), _TOKENS_SENDER_INTERFACE_HASH, sender);
0.5 IERC1820Implementer: ERC1820 实现者的接口
0.6 ERC1820Implementer:声明合约愿意成为某个接口的implement

1.ERC20
1.1 ERC20Burnable:带销毁amount,可销毁自己或别人(sender allowance够)的
1.2 ERC20Capped:增加cap(上限),mint的时候会校验totalSupply+amount<=cap
1.3 ERC20FlashMint:继承自IERC3156FlashLender,支持闪电贷的ERC20
1.4 ERC20Pausable:在_beforeTokenTransfer的时候验证paused接口,失败的无法转账ERC20
1.5 ERC20Permit:带permit(给某人提供有效的签名,则该人可以调用permit以允许spender使用你的代币)的ERC20
1.6 ERC20Snapshot:带余额检查点,每次转账会更新检查点中from和to的余额
1.7 ERC20Votes:带投票检查点的ERC20,继承自IVotes,ERC20Permit
1.8 ERC20VotesComp:继承自ERC20Votes,增加当前票数和上一次票数uint96的返回
1.9 ERC20Wrapper:增加一层包装,WERC20
1.10 ERC4626:ERC20资金池存取,赎回,mint等
1.11 IERC20Metadata:ERC20元数据接口(name,symbol,tokenURI)
1.12 IERC20Permit:允许spender花owner的token permit
1.13 ERC20PresetFixedSupply:在构造函数中预mint可销毁的ERC20
1.14 ERC20PresetMinterPauser:带权限控制(AccessControl)mint,pause的ERC20
1.15 SafeERC20:带transfer,transferFrom,approve等返回值校验的ERC20
1.16 TokenTimelock:带锁定的ERC20,只有超过_releaseTime,就可以将合约的余额转到_beneficiary地址,相当于一笔锁仓


2.ERC721
2.1 下标是从1开始的
2.2 合约不能接收erc721,如果要收需要继承ERC721Holder
2.3 ERC721Consecutive批量造币
2.4 IERC721Enumerable:下标访问nft token id
2.5 ERC721Burnable:带burn接口的ERC721
2.6 ERC721Votes:增加转账时from减票,to加票,及地址总票数接口
2.7 ERC721Pausable:在_beforeTokenTransfer的时候验证paused接口,失败的无法转账ERC721
2.8 ERC721Royalty:ERC721版税相关,继承IERC2981,带burn接口
2.9 ERC721URIStorage:将每一个tokenId的uri再存储一次,支持直接设置uri,不用管tokenId的规则
2.10 IERC721Metadata:ERC721元数据接口(name,symbol,tokenURI)
2.11 IERC721Receiver:增加onERC721Received,当接收到nft时执行,合约如果想要接收nft需要实现该接口
2.12 ERC721Holder:继承自IERC721Receiver,实现了onERC721Received

3.ERC777
3.1 ERC-777:在做send的时候如果有回调函数可以回调,以实现收款时的回调
3.2 send函数会检查持有者和接收者是否实现了相应的钩子函数,如果有实现(不管是普通用户地址还是合约地址都可以实现钩子函数),则调用相应的钩子函数
3.3 IERC777Recipient:当接收到777状态变化时调用表示已经接收到token,触发调用tokensReceived
3.4 IERC777Sender:在转账发送前调用tokensToSend
3.5 ERC777PresetFixedSupply:在构造的时候预mint


4.ERC1155
4.1 批量转nft
4.2 ERC1155Burnable:批量销毁1155
4.3 ERC1155Pausable:在_beforeTokenTransfer的时候验证paused接口,失败的无法转账
4.4 ERC1155Supply:增加_totalSupply的map,更新每个id(非tokenId)的总数
4.5 ERC1155URIStorage:将每一个tokenId的uri再存储一次,支持直接设置uri,不用管tokenId的规则,并且可以修改baseUri
4.6 IERC1155MetadataURI:根据token id返回uri元数据
4.7 ERC1155PresetMinterPauser:带权限控制(AccessControl)mint,pause的ERC1155
4.8 ERC1155Holder:批量接收持有1155,onERC1155Received,onERC1155BatchReceived
4.9 ERC1155Receiver:继承自IERC1155Receiver,但并未实现onERC1155Received,onERC1155BatchReceived

5.IERC1271:提供标准签名验证方法
6.IERC1363:调用transfer,transferFrom会触发接收者的onTransferReceived,调用approve会触发sender的onApprovalReceived
7.IERC1363Receiver:处理接收者的onTransferReceived
8.IERC1363Spender:处理sender的onApprovalReceived
9.IERC1822Proxiable:用于可升级合约,返加implement的槽
10.IERC2612:带permit的ERC20
11.IERC2981:nft版税,返回版税金额和receiver地址
12.IERC3156FlashLender:闪电贷
13.IERC3156FlashBorrower:接收到一个闪电贷后触发onFlashLoan
14.IERC4626:用于资金池存取
15.IERC2309:连续转账时,增加一个nft从from地址startTokenId到to地址的endTokenId的事件
16.ERC1967Upgrade:增加hash插槽存储逻辑合约地址,rollback的地址,admin地址,beacon地址
17.ERC1967Proxy:继承ERC1967Upgrade,返回implement

18.math
18.1 SafeCast:带安全检测的将高精度转低精度 uint256 a; uint128 b = toUint128(a);

19.crypto
19.1 ECDSA:验证消息是否由持有者签名指定地址的私钥
tryRecover:验证签名转地址
19.2 MerkleProof:处理默克尔树证明的验证
19.3 SignatureChecker:签名验证助手,可用于ECDSA.recover无缝支持来自外部拥有账户 (EOA) 的 ECDSA 签名以及来自 Argent 和 Gnosis Safe 等智能合约钱包的 ERC1271 签名
isValidSignatureNow:检查签名对于给定的签名者和数据哈希是否有效。如果签名者是智能合约,则使用 E​​RC1271 针对该智能合约验证签名,否则使用ECDSA.recover

20.Escrow(托管)
20.1 RefundEscrow:为受益人持有多方存入资金的托管。预期用途:参见Escrow。此处适用相同的使用指南。所有者账户(即实例化此合约的合约)可以存款、关闭存款期限,
并允许受益人提款或退款给存款人。与 的所有交互都RefundEscrow将通过所有者合约进行

21.数据结构
21.1 BitMap:用map实现某一位是否有设置
21.2 EnumerableMap:库中定义了多种可迭代map
* - `uint256 -> address` (`UintToAddressMap`) since v3.0.0
* - `address -> uint256` (`AddressToUintMap`) since v4.6.0
* - `bytes32 -> bytes32` (`Bytes32ToBytes32Map`) since v4.6.0
* - `uint256 -> uint256` (`UintToUintMap`) since v4.7.0
* - `bytes32 -> uint256` (`Bytes32ToUintMap`) since v4.7.0

    struct Bytes32ToBytes32Map {
        // Storage of keys
        EnumerableSet.Bytes32Set _keys; //keys是set可避免重复
        mapping(bytes32 => bytes32) _values;
    }

21.3 EnumerableSet:set,支持类型bytes32( Bytes32Set)、address(AddressSet) 和uint256()的集合
21.4 DoubleEndedQueue:用map实现的能够在序列的两端(称为前端和后端)有效地推入和弹出项目(即插入和删除)。在其他访问模式中,它可用于实现高效的 LIFO 和 FIFO 队列

22.utils
22.1 Checkpoints:实现block->value的数组存储
22.2 Create2:根据合约字节码,salt创建合约,如果构造函数是payable则可以转入amount
22.3 Address:方便判断是否是合约,转账,函数call,staticcall,delegatecall
判断是否是合约以下几点会误判断:
1.在构造函数中被判断是否是合约
22.4 Strings:uint转string,toHexString,判断两个string是否equal等

23. defaultOperators:特殊账户(通常是其他智能合约),这些账户将能够代表其持有者转移代币。如果您不打算在令牌中使用运算符,则可以简单地在构造函数传递一个空数组
operatorSend
operatorBurn


三.EVM
1.存储
1.1 ROM:用来保存所有EVM程序代码的"只读"存储,由以太坊客户端独立维护
1.2 Stack:即"运行栈",用来保存EVM指令的输入和输出数据,最大深度为1024,其中每个单元是一个"字",32字节,256位二进制数据,EVM大部分基础操作以"字"为单位
1.3 Memory:一个简单的字节数组,用于临时存储EVM代码运行中需要存取的各种数据,基于"字"进行寻址和扩展
1.4 Storage:其数据在组成一个存储树,树根会保存在账户状态数据中,每个账户的存储区域以"字"为单位划分为若干"槽",合约的"状态变量"会根据其具体类型分别保存到这些"槽"中

2.栈和内存操作码
2.1 POP:取出栈顶元素
2.2 PUSHn:把随后的nB的元素/item放入栈(可以直接在后边跟一个数字来指定要加入的字节数,取值范围为1到32,即最大为一个"字")
2.3 MLOAD:从内存中取出一个"字"
2.5 JUMP:修改程序计数器(PC)的位置
2.6 PC:程序计数器(program counter)
2.7 MSIZE:目前已激活(已使用)的内存大小(以"字"为单位)
2.8 GAS:可用的gas数量(当前交易内)
2.9 DUPn:复制栈里的n个元素到栈顶(可以直接在后边跟一个数字来指定要复制的元素,比如DUP6可以复制栈顶数第6个元素到栈顶,后边跟的数字最大为16).
2.10 SWAPn:交换栈里的第1个元素和第n个元素(可以直接在后边跟一个数字来指定要交换的元素,比如SWAP6可以将栈中的第7个元素与栈顶元素交换,后边跟的数字大最为16).

3.通用系统操作码
3.1 CREATE:创建账户
3.2 RETURN:终止执行,并返回输出数据,return(p,s):终止运行,返回mem[p,p+s]的数据
3.3 REVERT:终止执行,并复原(回退)状态改动,revert(p,s):终止运行,撤消状态变化,返回mem[p,p+s]的数据
3.4 SELFDESTRUCT:终止执行,并将账户加入当前交易的自毁集合

4.算术操作码
4.1 ADD:加法
4.2 MUL:乘法
4.3 SUB:减法
4.4 DIV:整数除法
4.5 SDIV:有符号整数除法
4.6 MOD:模运算
4.7 SMOD:有符号模运算
4.8 ADDMOD:先做加法再取模,addmod(x,y,m):(x+y)%m
4.9 MULMOD:先做乘法再取模,mulmod(x,y,m):(x-y)%m
4.10 EXP:指数运算
4.11 STOP:终止操作,return(0,0)

5.环境数据操作码
5.1 ADDRESS:当前程序执行所基于的账户地址
5.2 BALANCE:取得指定账户的余额
5.3 CALLVALUE:取得当前交易的转账金额(交易数据中的value字段值)
5.4 ORIGIN:取得最初引发这次执行的原始交易的发送者地址
5.5 CALLER:当前程序执行的直接调用者
5.6 CODESIZE:当前运行环境的代码长度
5.7 GASPRICE:当前运行环境的gasPrice
5.8 EXTCODESIZE:指定账户的代码长度
5.9 RETURNDATACOPY:复制前一个内存调用的输出数据

6.消息调用码
6.1 CALL:向某个接收者地址发起消息调用,call(g,a,v,in,insize,out,outsize):使用mem[in,in+insize]作为输入数据,提供g gas和v wei作为地址a发起消息调用,输出结果数据保存在mem[out,out+size],发生错误时返回0,正确结束返回1
6.2 CALLCODE:与CALL基本等价,recipient和sender相同,code address相同(不需要切换执行环境)
6.3 DELEGATECALL:与CALL基本等价,recipient和sender相同,code address不同,不允许进行转账,且执行环境保持不变.执行其它合约的代码,但是使用调用合约的存储
A合约调用B合约时,B所读写持久化状态存储其实是在A合约里面的,或者说,A把自己的存储授权给了B,请它来修改。
6.4 STATICCALL:与CALL基本等价,不允许进行转账,被调用的合约的持久化状态存储将被设置为只读的,无法修改.应用于contract只读方法,即view或pure方法,否则将抛出异常。


CALLCODE和DELEGATECALL允许在不切换执行环境的情况下执行其他合约函数,也就是允许我们基于当前合约的上下文来执行其他合约中的代码,使我们
可以通过所谓的"库(library)"函数来重用一些通用代码

7.内存结构
7.1 0x00-0x3f:为哈希方法而保留空间,0x40:空闲内存指针
7.2 0x40-0x5f:当前分配的内存大小(如内存空闲指针),0x60:空闲内存起始地址
7.3 0x60-0x7f:初始值为0的存储槽

8.类型定义
8.1 address:等同于uint160

9.gas消耗
9.1 sstore:20000GAS,相当于基础操作的5000倍

10.槽:每个合约有2的256次方个槽,相当于10的77次方个

11.调用数据操作码与环境操作
11.1 calldatasize:返回交易数据的大小,获取当前执行环境的调用数据的字节大小
11.2 calldataload:导入32B的交易数据放到栈,获取当前环境的调用数据,calldataload(p):位置p的调用数据
11.3 calldatacopy:复制一定字节数的交易数据到内存,复制当前执行环境的调用数据到内存,calldatacopy(t,f,s):将调用数据的位置f复制s个字节到内存的位置t
11.4 extcodesize:获取指定账户的关联代码字节大小,extcodesize(a):地址a的代码大小
11.5 extcodecopy:复制指定账户的关联代码到内存中:和codecopy(t,f,s)类似,但从地址a获取代码
11.6 returndatasize:获取当前环境中前一次调用的返回值数据字节大小,最后一个returndata的大小
11.7 returndatacopy(t,f,s):将前一次调用的返回值数据复制到内存中,从returndata的位置f复制s个字节到内存的位置t

12.内存操作码
12.1 mload:从内存导入一个字到栈上,mload(p):mem[p,p+32]
12.2 mstore:存储一个字到内存,mstore(p,v):mem[p,p+32]=v,(0x40,0x60)内存前64字节是系统保留来做sha3哈希运算,是EVM的保留区域
12.3 mstore8:存储一个字节到内存,mstore8(p,v):mem[p] = v & 0xff(仅修改一个字节)
12.4 jump:修改程序计数器,jump(label):跳转到标签位置
12.5 jump1:根据条件满足与否决定是否修改程序计数器,jumpi(label,cond):如果条件非0,跳转到标签
12.6 pc:获取当前程序计数器的值(执行指令之后的值),当前代码位置
12.7 msize:获取当前已分配内存的字节大小
12.8 jumpdest:标记一个有效的跳转目标
12.9 sload(p):storeage[p]
12.10 sload(p,v): v=storage[p]

13.运算
13.1 SLT:带符号小于,slt(x,y):如果x<y返回1,否则返回0
13.2 SGT:带符号大于
13.3 ISZERO:算数非,iszero(x),如果x==0则为1,否则为0
13.4 NOT:按位非
13.5 BYTE 获取一个字中的某个字符
13.6 shl(x,y):将y逻辑左移x位
13.7 shr(x,y):将y算术右移x位
13.8 sar(x,y):将y算术右移x位
13.9 signextend(i,x):对x的最低位到第(i*8+7)位进行符号扩展

14.系统操作
14.1 CREATE:使用给定的关联代码创建一个合约,create(v,p,s):用mem[p,p+s]中的代码创建一个新合约,发送v的gas返回新地址
14.2 CREATE2(v,n,p,s):用mem[p,p+s]中的代码在地址keccak256(addr.n,keccak256[mem[p,p+x]])上创建新合约,并发送v的gas后返回新地址
14.2 CALL:发起一个消息调用(如果附带eth需要额外收gas)
14.3 CALLCODE:从指定地址获取代码,在当前执行环境中执行
14.4 RETURN:停止执行并返回输出数据
14.5 DELEGATECALL:与CALLCODE等价,且保持caller与value不变
14.5 STATICCALL:与CALL等价,但附带的value为0,且不能修改状态(即不允许create,create2,LOG0-4,sstore,selfdestruct等opcode)
14.6 REVERT:停止执行
14.7 INVALID:预设的非法指令
14.8 SELFDESTRUCT:停止执行并将当前账户添加到自毁列表


四.进阶
1 eth的四棵树:交易树和收据树是区块级别,状态树和存储树是全局唯一
a.交易树:存储block index,block hash,tx hash等信息
b.收据树(交易执行过程中的一些数据):hash->value,由4个元素组成
* 交易执行之后的状态树根节点hash
* 区块中当前交易执行完成后的累积gas使用量
* 交易过程中创建的日志集合
* 由交易日志所构成的Bloom过滤器
c.状态树(账户的地址、余额、nonce、合约代码hash等状态信息,记录到状态数据库):有四种节点,分别是空节点、叶子节点、扩展节点和分支节点
d.存储树:(保存了与某一智能合约相关的数据信息。由账户状态保存账户存储树的根节点哈希值(在 storageRoot 字段)。每个账户都有一个账户存储树。)
* nonce
从此地址发送出去的交易数量(如果当前为 EOA 账户)或者此账号产生的合约创建操作(现在先别管合约创建操作是什么)。
* balance
此账号所拥有的以太币数量(以 Wei 计量)。
* storageRoot
账户存储树的根节点哈希值
* codeHash
对于合约账户,就是此账户存储EVM代码的哈希值。对于EOA账户,此处留空。
* 如果要取存储树中保存的某个特定合约的状态变量数据,需要address,storage position,block num,树中保存的具体数据的叶节点是根据这3个输入计算得出,如果只有2个变量, storage position可以用0,1表示,
也可以是一个hash值,用来指定合约存储中特定位置的mapping的key对应的数据

交易会附带合约创建或者调用的ABI编码,这些代码会在以太坊虚拟机执行


2.返还:当在代码处理中导致EVM执行特定的指令(SSTORE)将合约账户的存储内容重置为0时,会返回15000Gas,抵消一部分执行费用,如果有自毁的合约,则会返回26000Gas

3.合约地址生成:
3.1 EVM会根据发送者地址和nonce经过RLP编码后再进行keccak256得到hash的低160位(即96到255)共计20字节作为生成合约的地址;
3.2 如果你指定了一个可选的salt(一个bytes32值),那么合约创建将使用另一种机制(create2)来生成新合约的地址:它将根据给定的盐值,创建合约的字节码和构造函数参数来计算创建合约的地址。
bytes32 salt = keccak256(abi.encodePacked(l1ERC20, beacon));
ClonableBeaconProxy createdContract = new ClonableBeaconProxy{ salt: salt }();


4.合约部署
合约代码->先经过编译器优化生成类似汇编的evm字节码(0101)->字节码上链->进入到evm中会将字节码转成操作码->然后将操作码放入栈中->变成汇编指令->通过解释器来解释执行
solc编译后,变成类似于汇编的EVM指令
Interpreter定义了一套完整的指令集

5.可升级合约的存储:storage合约每次都要继承上一次的storage合约
5.1 继承存储:代理合约和逻辑合约使用相同的存储,升级合约继承逻辑合约

5.2 永久存储:数据定义在代理合约和逻辑合约都能继承的合约里.代理合约可以定义自己的状态变量,不用担心被覆盖.逻辑合约则不能再定义别的状态变量了

5.3 非结构化存储:和继承存储类似,继承存储必须要继承要升级的状态变量,使用代理合约定义和保存要升级的数据

即设定一个固定的slot用于存储impl地址,与合约中的其他全局变量顺序无关。

在EIP-1967中,该固定的slot地址被标准化为:

逻辑合约地址:0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc

bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)
信标合约地址:0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50

bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1)
Admin地址:0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103

bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1)
这使得像Etherscan这样的浏览器可以很容易地识别这些代理(因为任何在这个非常具体的插槽中具有类似地址值的合同都很可能是一个代理),并解析出逻辑合约地址。

5.4 append-only
使用append-only的储存合约。在该种模式下,状态存储在单独的存储合约中,并且只能追加,不允许删除。Impl合约则继承该存储合约,以便于使用相应的状态。然后,每次需要增加一个新的状态变量时,都可以扩展存储合约

6.create2
提供了一种预先确定合约地址的方法。这个方法在推导出优化gas的合约地址和实现像状态通道这样的扩展解决方案时非常有用。在可升级性方面,create2 提供了创建可变合约(metamorphic contract)的能力,这些合约可以用新的字节码重新部署到同一地址

7.升级合约的方式:
7.1 Transparent:
a.如果Proxy合约发现自己被ProxyAdmin合约调用,那么它会调用自身的函数代码;如果调用者是ProxyAdmin之外的账户,那么它会通过delegatecall去调用Implementation的代码。这样就保障了合约升级的安全性。
b.升级函数在proxy里面,proxy里面的函数基本都有ifAdmin修饰符校验

7.2 UUPS:
a.Proxy直接把所有的请求都通过delegatecall丢给Implementation(如果是升级,Implementation的升级函数会确认一下是否为管理员)
b.需要实现_authorizeUpgrade函数以确保只有管理员账户可以进行升级
c.升级函数upgradeTo在Implementation合约中

7.3 Beacon
a.Implementation地址并不存放在Proxy合约里,而是存放在Beacon合约里,Proxy合约里存放的是Beacon合约的地址
b.在合约交互的时候,用户同样是和Proxy合约打交道,不过此时因为Proxy合约中并未保存Implementation地址,所以它要先访问Beacon合约获取Implementation地址,然后再通过delegatecall调用Implementation。
c.在合约升级的时候,管理员并不需要和Proxy合约打交道,而只需要交互Beacon合约,把Beacon合约存储的Implementation改掉就行了。
d.就是多个Proxy共享相同的Implementation、需要批量升级的场景。此时,如果想把所有Proxy都升级,那么升级Beacon就天然可以达到升级所有Proxy的效果


Transparent VS UUPS

Transparent把升级逻辑放在Proxy中,而UUPS把升级逻辑放在implementation中,这样的差异会造成哪些使用体验上的不同呢?

(1)Transparent需要多部署一个ProxyAdmin合约,所以如果不考虑后续升级,那么Transparent是更消耗gas的。但是如果考虑后续升级,情况就不是这样了。因为对于UUPS而言,升级的逻辑代码需要出现在后续的每一个版本的Implementation合约中(否则合约将失去可升级特性),所以如果考虑到多次升级,反而是Transparent更加省gas。

(2)对于升级之外的合约交互,Transparent总是需要判断是否来自ProxyAdmin账户(否则它没法知道是升级还是普通交互),因此这里会多一次storage load操作。而UUPS是把包括升级在内的所有交互都无脑丢给implementation,所以不需要这个判断。因此,对于普通用户而言,UUPS总是会更省gas一些。

(3)刚才有说到,升级的逻辑代码需要出现在后续的每一个版本的Implementation合约中,但实际上也未必。有可能在某个版本,我们决定这就是最终版本,以后再也不会再升级了,那么我们就不给这个版本添加升级函数,这样一来可升级合约就变成了不可升级合约。而Transparent的可升级特性是不可取消的(当然可以有其他间接方式做到,比如放弃ProxyAdmin合约的管理权限),所以UUPS相对Transparent具有更大的灵活性。
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics