# 初级 ## constant和immutable修饰的状态变量区别? 1. constant和immutable都用于修饰不可变变量,区别在于初始化时间。constant需要在定义时就初始化,而immutable可以推迟到constructor中。 2. constant和immutable的值都被存储在字节码中。 ## private、internal、public以及external修饰符的区别? 四个关键字修饰的可见性不同。 ● private关键字修饰的仅在合约内部可见。 ● internal关键字修饰的在合约内部可见的同时,还在其派生合约中可见(也即继承了当前合约的子合约)。 ● public关键字修饰的在合约内部可见的同时,还在外部合约中可见。 ● external关键字修饰的仅在外部合约可见。 ## 智能合约的大小有上限吗?有的话上限是多少? 智能合约的大小存在上限,其上限为24.576kb。 ## 简述create和create2之间的区别 create和create2都是创建合约的操作码。它们之间的主要区别是:create2操作码生成的合约其地址是可预测的,而create操作码生成的合约其地址无法预测。 具体地说: create操作码生成的合约地址为:`keccak256(abi.encodePacked(address_deployer,nonce))`。由于nonce实时变化,因此create生成的合约地址无法预测。 而create2操作码生成的地址为: `keccak256(abi.encodePacked(bytes1(0xff),address_deployer,salt,contract_bytecode))`。这里的0xff用于避免生成的地址与create冲突,由于四个参数都是可预测的,因此create2操作码生成的地址也可预测。 ## Solidity0.8.0后,是否还有可能出现整数溢出情况? 1. 合约在编写时显示声明了unchecked关键字(在该unchecked标注的代码块中不会增加溢出检查)。 2. 在内联汇编中,不会触发溢出检查。 3. 如果存在大类型向小类型转换,也会出现整数溢出。 4. 移位运算中不进行整数溢出检查。 ## 简述代理模式 代理模式是为了解决智能合约代码部署上链后无法升级设计的。 代理模式将数据和逻辑分隔,数据保存在代理合约中,而业务逻辑保存在逻辑合约中。当用户与实现了代理模式的项目进行交互时,首先调用代理合约,而后代理合约读取逻辑合约地址,并对其进行委托调用。由于代理合约中的逻辑合约地址可以被修改,因此可通过将代理合约中的逻辑合约地址指向新的逻辑合约来完成升级。 ## EIP1559之前以太坊gas计费机制 在EIP1559之前,一笔交易的gas费用为`gasUsed*gasPrice`。此时,所有的gas费用都归矿工所有。 在EIP1559之后,一笔交易的gas费用还是`gasUsed*gasPrice`,但是这里的`gasPrice`被分为`base_fee`以及`tips_fee`。 其中`base_fee`指的是当前区块`gasPrice`的底线,`gasUsed*base_fee`这一部分会被燃烧掉。而`tips_fee`指的是用户愿意支付给矿工的小费,`gasUsed*tips_fee`这部分归于矿工所有。 ## 为什么不能用tx.origin进行身份验证? tx.origin代表交易的发送者,它无法准确代表当前调用者。考虑EOA `CALL` A `CALL` B的情况,B中存在身份验证,要求调用者必须为A。如果在B中使用tx.origin进行调用者验证,tx.origin为EOA,则无法调用成功。但B的调用者实际为A,因此导致身份验证有误。 ## tx.origin与msg.sender之间的区别 tx.origin代表交易的发起者,是EOA账户。 msg.sender代表当前调用者,可能是EOA账户也可能是合约账户。 ## ether,gwei,wei 1 ether = 10^18 wei 1 gwei = 10^9 wei ## 简述fallback和receive之间的区别 receive只在收到ETH且msg.data为空时调用,receive必须用payable修饰。 fallback在交易没有函数匹配时调用,或者收到ETH但没有receive函数时调用(此时fallback需要用payable修饰)。 ## 如何向没有receive,fallback以及payable修饰的合约发送eth? 构建一个合约,而后在合约中执行selfdestruct操作,参数填写待发送合约。selfdestruct操作会销毁当前合约,而后将合约的所有eth发送给指定地址(不受限制)。 注意,当前的selfdestruct操作码变为: 1. 只有在合约的constructor函数内部使用selfdestruct,才会真正从区块链中移除合约相关数据 2. 非上述情况,则使用selfdestruct只会将剩余余额转移到指定地址,而不会移除数据,且在Cancun升级后,执行了selfdestruct的合约依然可以被调用。 ## transfer和send的区别是什么?为什么不推荐使用? transfer和send都是向指定地址发送ETH的函数,他们是payable address类型的成员函数。 transfer和send之间的区别:send在出现错误时不抛出错误而是返回false,而transfer会在出现错误时抛出错误,实际上transfer(x)相当于require(send(x))。 transfer和send都限制在2300gas,在转账时容易失败,因此不推荐使用。 ## 什么是代理合约中的存储冲突? 由于代理合约会对逻辑合约进行委托调用,因此在代理合约中进行状态变量修改时依赖于逻辑合约的状态变量布局。如果代理合约与逻辑合约的状态变量布局不同,可能导致状态变量被错误的读取或写入。 举例来说:代理合约的状态变量布局为:`uint256 a;byte32 b;uint256 c;`而逻辑合约的状态变量布局为:`uint256 a;uint256 c;byte32 b;`。如果代理合约委托调用逻辑合约修改状态变量c,会错误的修改slot为2的状态变量,实际上c的slot为3。 ## abi.encode 和 abi.encodePacked 之间有什么区别? 在编码静态类型时:abi.encode会在参数长度不满32字节时,对参数进行填充至32字节后进行拼接;abi.encodePacked则不进行填充,而是直接拼接。在编码动态类型时:abi.encode会包含长度字段,而abi.encodePacked不包含长度字段。 注意:abi.encodePacked被称为非标准打包模式,其存在哈希冲突问题,例如: ``` bytes32 hash1 = keccak256(abi.encode(0x123, 0x456)); bytes32 hash2 = keccak256(abi.encode(0x1, 0x23456)); ``` hash1和hash2的值是相同的,因为非标准打包模式不会对参数进行填充! ## 什么是三明治攻击? 攻击者发现一笔大规模的买入交易,而后攻击者按照下述步骤进行攻击: 1. 攻击者在用户买入交易执行之前,也执行一笔买入交易,购买相同代币。 2. 用户买入交易执行。 3. 攻击者出售买入的代币。 由于用户大规模买入,导致代币的价格上升,攻击者抢先在价格较低时购买该代币,后续价格抬高后出售该代币。 ## 在EIP-1559中,basefee如何确定? basefee根据区块的gas使用率动态确定。当区块gas使用率超过 50% 时,下一个区块的 basefee 会自动升高,最高比例为 12.5%(使用率100%),当区块gas使用率低于 50% 时,下一个区块的 basefee 会自动降低,最高比例为 12.5%(使用率0%)。升高和降低的基准是前一个区块的basefee。 # 中级 ## warm read和cold read的区别是什么? cold read指第一次访问状态变量,消耗2100gas,而warm read指再次访问状态变量,消耗100gas。 ```solidity! // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.26; contract Read { // Occupies storage slot 0 uint256 number = 10; function getNumber() external view returns(uint256) { // 第一次访问,冷读,消耗2100gas require(number == 10); // 第二次访问,热读,消耗100gas return number; } } ``` ## 代理模式中的函数选择器冲突是什么?为什么会发生? 在代理模式中,用户向代理合约发起函数调用,而后代理合约对逻辑合约进行委托调用。如果代理合约和逻辑合约中存在函数选择器相同的函数,将导致本应该委托调用逻辑合约却直接调用了代理合约。 ## payable函数对gas有什么影响? 假设当前有两个函数的逻辑完全相同,一个声明为payable,一个未声明。则声明为payable的函数其消耗的gas更低、字节码更短。原因是非payable函数需要检查msg.value是否为空,并在任何发送ETH的逻辑执行时revert,因此存在更多的检查,导致gas消耗增大。 ## 什么是签名重放攻击? 签名重放攻击是指,攻击者复制用户对交易的签名,并将该签名重复利用使得该交易被重复执行。 签名重放攻击分为: 1. 同链重放:复制交易签名,在当前链中重放交易。(使用nonce值规避) 2. 不同链重放:复制交易签名,在不同链中重放交易。(使用chainId规避) ## 重点 ## 简述solidity内存模型 在solidity中,内存中的0x00-0x7f都属于预留空间,他们的作用分别为: ``` 0x00-0x3f:用户哈希方法的临时空间(暂存空间)。 0x40-0x5f:空闲内存指针,指向当前内存的空闲空间。 0x60-0x80:零插槽,用于存储内存动态数组的初始值(初始值就是0值),不能写入。 ``` 注意点: 1. 暂存空间可以在内联汇编中被使用。 2. solidity中如果要写入大于64字节的新的内容,需要从0x80开始。 3. 动态内存数组的元素都是32字节的整数倍大小(除了bytes和string类型外)。 4. 空闲指针指向的位置不一定是全0的未初始化区域。 `在Solidity中有一些操作需要一个大于64字节的临时内存区域,因此它们不会适配到临时空间中。它们会被放置在空闲内存指针指向的位置,但由于它们的生命周期很短,指针不会被更新,因此空闲指针指向的内存可能已经被清零,也可能没有。因此,不应该期望自由内存指向的是一个已经被清零的内存区域。虽然使用msize来找到一个肯定已经被清零的内存区域似乎是个好主意,但是如果没有更新自由内存指针而长期使用这样的指针,可能会有意想不到的结果。` ## 函数参数中的memory和calldata修饰符有什么区别? 使用memory修饰的函数参数将存储在内存中,参数的值是可变的;而calldata修饰的函数参数将存储在calldata中,参数的值是不可变的。 ```solidity contract test_memory_calldata { function test1(bytes memory param1, bytes calldata param2) public { param1 = "123"; // 此处报错 //TypeError: Type literal_string "123" is not implicitly convertible to expected type bytes calldata param2 = "123"; } } ``` ## 描述sstore指令的gas消耗 如果当前写入的槽为cold,需要多消耗2100gas,gas消耗如下: 1. 从零值到非零值:20000+2100 = 22100gas 2. 从非零值到非零值:2900+2100 = 5000gas 3. 从非零值到零值:2900+2100 = 5000gas,但是存在gas返还。 如果当前写入的槽为warm,则上述gas消耗都减去2100即可。 1. 将一个非零值的slot覆盖为零值,则可以得到gas refund。 2. 在伦敦升级之前,selfdestruct也可以得到gas refund,但是现在已经被移除。 ## 如果合约delegatecall零地址或已经selfdestruct的合约,会发生什么?如果是低级调用而不是委托调用怎么办 合约delegatecall零地址会返回true,因为零地址是一个EOA地址,delegatecall允许调用EOA地址,会默认返回true。如果delegatecall一个已经执行selfdestruct的合约,需要根据调用的函数分情况讨论,如果调用的函数在合约中存在,且执行无revert,则返回true;如果执行revert则返回false;如果调用的函数在合约中不存在,且合约未实现fallback函数,则返回false。 ## 简述ERC777代币存在的漏洞 ERC777会在发送代币前执行发送者的hook函数,在发送代币后执行接收者的hook函数,这需要发送者和接收者实现特定的接口,ERC777通过EIP1820注册表检测发送者或接收者是否实现特定函数。这就导致,发送者或接收者可以在合约内部植入恶意逻辑。具体的攻击手段如下:发送者hook函数:在发送者hook函数中植入恶意逻辑,进行重入攻击。例如imBTC事件。 ## ERC721A 如何降低铸币成本?权衡是什么? ERC721A中的tokenId是连续递增的,当用户需要批量mint时,只需要将连续的tokenId中,首个tokenId的owner设置为用户即可,其余tokenId无需设置owner,通过减少状态变量的写入来降低批量mint的成本。 权衡:当ERC721A进行transfer或transferFrom时,可能导致更多的gas成本。因为在ERC721中转移代币所有权只需要修改一个状态变量,而ERC721A需要修改两个。 在传统的ERC721代币中,需要循环执行_mint方法来实现mint多个nft。ERC721A为了降低mint多个nft的gas消耗,更新了mint函数的逻辑,只需要输入接收者以及要mint的代币数量即可完成。 举例来说:用户A需要mint10个nft代币,ERC721A维护了一个token数组,假设当前tokenId已经分配到10,那么从tokenId为11到tokenId为20这些token都将属于用户A。ERC721A只是将tokenId为11的nft的owner设置为用户A,后续的token都不设置owner。 ERC721A如果需要找到某个NFT的owner,只需要从当前tokenId开始,将token数组递减遍历,直到找到第一个有owner的NFT,该NFT的owner就是要找的owner。 ERC721A的transfer操作解释:假设当前用户A拥有tokenId为2,3,4的nft,现在需要将tokenId为3的token transfer给用户B,操作如下: 1. 将tokenId为3的nft owner设置为B。 2. 将tokenId为4的nft owner设置为A。 ## 什么是ERC165?什么是EIP1820? ERC165提案提出了一个标准方法,使得可检测智能合约实现了哪些接口。举例来说: 合约A实现了ERC20,并且兼容ERC165,则合约A需要实现supportsInterface方法。下面的interfaceID的计算方式为:ERC20接口的所有函数选择器进行求或,最后取bytes4。 ``` interface ERC165 { /// @notice 查询一个合约时候实现了一个接口 /// @param interfaceID 参数:接口ID, 参考上面的定义 /// @return true 如果函数实现了 interfaceID (interfaceID 不为 0xffffffff )返回true, 否则为 false function supportsInterface(bytes4 interfaceID) external view returns (bool); } ``` EIP1820定义了一个接口注册表,用户可在EIP1820中注册某个合约地址实现了哪些接口。EIP1820是中心化的。 ## 什么是rebase token? Rebase 代币是流通量不断变化的加密资产,用户拥有的资产以代币总供应量的百分比衡量,如果rebase代币增发,则用户的资产也将扩张。 例子:代币 X 有 100 万个代币在流通。作为主要投资者,我的钱包里持有 10% 的代币,即 100,000 个代币。该项目具有重新定基的属性,代币数量每天都会增加 10%。1 天后,流通代币数量增加到 110 万,我的持币量增加到 110,000,增加了 10,000,这仍然是供应量的 10%。第二天之后,供应量再次增加到 121 万,我现在有 121,000 个代币,增加了 11,000,这意味着我仍然持有总供应量的 10%。这种情况持续下去,增加(或减少)会自行累积,从而导致巨大的代币通胀或通缩。使用重新定基的项目通常将其作为对代币持有者的奖励,并激励投资者持有代币而不是出售。重要的是要记住,代币通胀/通货紧缩会影响代币价格,尤其是从长远来看。 ## uint64[] a=[1,2,3,4]在storage中占用几个slot?,在memory中占用多少字节? storage:假设当前数组定义在slot=1的位置,slot=1的位置将存储数组长度,而后数组的元素将从slot=keccak(1)开始顺序存储,由于a数组中的各元素为uint64类型,并且刚好4个元素,则占用1个slot,因此总共占用两个slot。 memory:在memory中存储数组时首先会存储数组的长度,占用32字节,而后顺序存储数组的元素,在memory中,数组的每个元素都会占32字节,因此总共占32*(4+1)=160字节。 字符串 string 和 bytes 实际是一个特殊的 array ,编译器对这类数据有进行优化。如果 string 和 bytes 的数据很短。那么它们的长度也会和数据一起存储到同一个插槽。 具体为: 1. 如果数据长度小于等于 31 字节, 则它存储在高位字节(左对齐),最低位字节存储 length * 2。 2. 如果数据长度超出 31 字节,则在主插槽存储 length * 2 + 1, 数据照常存储在 keccak256(slot) 中。 ## 为什么要有invalid操作码? INVALID操作码保证了合约试图执行未定义的操作时,会安全地停止执行。 1. INVALID会消耗完所有gas,并回滚所有状态。 2. 在EVM执行过程中如果执行了一个未定义操作码,也将最终指向INVALID操作码。 ## call、delegatecall、staticcall、callcode的区别 1. call:在被调用合约的上下文中执行。 2. delegatecall:借用被调用合约的代码,在调用合约上下文中执行。 3. staticcall:在被调用合约的上下文中执行,但不改变任何区块链状态。 4. callcode:老版的delegatecall。