ques easy
Solidity 问题汇总
1. 私有、内部、公共和外部函数之间的区别?
在 Solidity 中,函数可以根据其可见性被标记为私有(private)、内部(internal)、公共(public)和外部(external)。这些可见性关键字决定了其他合约和账户可以如何以及是否可以调用 函数。
Private(私有)
- 私有函数只能在定义它们的合约内部被调用。这意味着它们对于派生合约和其他外部调用者都不可见。
- 私有函数被标记为
private。
Internal(内部)
- 内部函数可以在定义它们的合约内部以及派生合约中被调用。它们对于外部合约和账户不可见。
- 内部函数被标记为
internal。 - 内部函数和状态变量可以通过继承被子合约访问。
Public(公共)
- 公共函数可以在合约内部被调用,也可以通过消息调用从外部调用。这意味着任何人都可以调用公共函数。
- 公共函数被标记为
public。 - 设置函数为公共会自动生成一个函数选择器,使得它可以被外部账户和合约调用。
External(外部)
- 外部函数是为了从合约外部被调用而设计的。它们不能被合约内部的其他函数(通过
this访问)直接调用,除非使用this.functionName()的语法。 - 外部函数被标记为
external。 - 外部函数通常在接口中使用,并且在需要确保函数只能从外部调用时使用。
总结和比较
private和internal函数不会暴露给外部调用,它们只能被合约内部(对于private)或派生合约(对于internal)访问。public和external函数可以从外部调用,但public函数也可以在合约内部调用,而external函数在合约内部调用时需要特殊语法。external函数对于某些类型的参数比public函数更有效率,因为它们可以直接访问calldata,这是一个低成本的数据区域。
在设计合约时,选择合适的函数可见性是很重要的,因为它影响到合约的接口设计、安全性和功能性。通常,你应该尽可能地限制函数的可见性,只有当需要从外部访问时才将函数设置为public或external。
2. 智能合约大小大约可以有多大?
智能合约的大小限制主要由 Ethereum 区块链上的 EVM(以太坊虚拟机)字节码大小限制决定。截至我的知识截止日期(2023 年 4 月),智能合约的大小限制是 24KB(即 24,576 字节)。
这个限制是由 EIP-170(Ethereum Improvement Proposal 170)引入的,旨在 防止在单个区块中包含太多的代码,因为这可能会对网络的性能造成负面影响。具体来说,这个限制适用于合约创建交易中的init代码,即在合约创建时部署到区块链上的代码。
如果合约的大小超过了这个限制,你将无法部署它,因为交易会因为超出气体限制而失败。如果你遇到这个问题,你可能需要考虑优化合约代码,或者将合约分割为多个更小的合约,并通过合约之间的调用来实现功能。
在实践中,大型智能合约通常通过模块化设计,将逻辑分散到多个合约中,以避开这个限制。这种方法还有助于提高代码的可维护性和可升级性。
3. create 和 create2 之间有什么区别?
在 Solidity 和以太坊智能合约中,create和create2是两个用于创建新合约实例的不同操作码。它们之间的主要区别在于它们如何决定新创建的合约的地址。
CREATE
create操作码是以太坊最初提供的方式来创建一个新的合约。- 使用
create时,新合约的地址是通过部署合约的地址和该地址发起的交易数量(即 nonce)来确定的。 - 公式是:
keccak256(rlp.encode(deployer_address, nonce))。 - 这意味着,只要部署者的地址和其 nonce 不变,使用
create创建的合约地址总是相同的。
CREATE2
create2是后来通过 EIP-1014(以太坊改进提案 1014)引入的,它增加了创建合约时更多的灵活性和确定性。- 使用
create2时,新合约的地址是通过部署合约的地址、一个由部署者提供的盐值(salt)、和合约初始化代码的哈希来确定的。 - 公式是:
keccak256(0xff + deployer_address + salt + keccak256(init_code))[12:]。 - 这种方法允许在合约创建之前就预测合约地址,并允许使用相同的部署者地址和初始化代码多次部署在不同地址的合约,只要盐值不同。
主要区别
- 地址预测:
create2允许在部署之前预测合约地址,而create只能在交易完成后确定地址。 - 冲突处理:
create2可以使用不同的盐值来避免地址冲突,而create方式下,如果两个合约有相同的部署者和 nonce,它们将有相同的地址。 - 状态清理:
create2允许在地址空间中“清理”状态,即可以删除一个合约并使用相同的盐值和部署者重新将一个新合约部署到同一个地址。
使用场景
create适合简单的合约创建场景,其中合约地址的预测不是必须的。create2非常适合需要复杂部署逻辑的场景,例如在 Layer 2 解决方案、智能合约钱包(如 MetaMask 的多重签名钱包)、以及需要确定性部署方法的其他复杂智能合约系统中。
开发者会根据具体的应用场景和需求选择使用create还是create2。
4. Solidity 0.8.0 版本对算术运算有什么重大变化?
Solidity 0.8.0 版本引入了几个重要的变化,特别是关于算术运算的处理。这些变化主要集中在如何处理算术运算中的溢出和下溢问题。在 Solidity 0.8.0 之前的版本中,合约开发者需要使用 SafeMath 库或者手动检查溢出和下溢,以确保算术运算的安全性。但是从 0.8.0 版本开始,编译器内置了这些检查。
以下是 Solidity 0.8.0 中对算术运算的主要变化:
-
内置的溢出和下溢检查:在 Solidity 0.8.0 及以后的版本中,所有的算术运算默认都会检查溢出和下溢。如果运算结果超出了变量类型能表示的范围,将会触发一个运行时错误(revert)。这意味着使用如
uint256或int256等类型进行的加法、减法、乘法和除法运算都自带了这些检查,而无需开发者额外编写代码来防范这些风险。 -
新的类型
unchecked:由于默认的溢出检查会增加一些额外的气体成本,Solidity 0.8.0 引入了unchecked块。在unchecked块内的运算不会进行溢出和下溢检查,这允许开发者在确定运算不会溢出或下溢的情况下节省气体。例如:unchecked {
uint256 a = 1;
uint256 b = a - 2; // 不会检查下溢,即使结果是负数(在 uint256 中表现为极大的正数)
} -
类型转换的改变:在 Solidity 0.8.0 中,类型转换的规则也有所改变。现在,进行类型转换时,如果目标类型不能包含原始值(例如,将一个很大的
uint256转换为uint8),也会触发运行时错误。
这些变化使得合约代码的安全性提高,因为它减少了因为算术错误导致的安全漏洞的可能性。同时,它也意味着开发者在写代码时需要更加注意,以避免不必要的运行时错误,并且在性能优化时考虑使用 unchecked 块。
5. 代理需要哪种特殊的 CALL 才能工作?
在以太坊智能合约中,代理(Proxy)合约通常用于实现可升级合约、委托调用或者逻辑分离。代理合约工作的核心是通过特殊的CALL操作来转发外部调用到另一个合约(通常称为逻辑合约或实现合约)。这个CALL操作是通过 Solidity 的底层call函数来实现的,它允许合约以动态的方式调用其他合约的函数。
为了使代理工作,通常会使用以下的CALL操作:
(bool success, bytes memory data) = targetContractAddress.call{value: msg.value}(calldata);
这里的targetContractAddress是逻辑合约的地址,calldata是一个包含了函数选择器和参数的字节序列。
这个call操作的特性包括:
- 它可以发送以太币(通过
{value: msg.value})。 - 它不限制 gas 的使用,除非显式地指定。
- 它返回一个布尔值
success来指示调用是否成功,以及一个字节序列data,其中包含了调用的返回数据(如果有的话)。 - 它允许调用合约的任何函数,包括那些不存在的函数(因为
calldata是动态生成的)。
代理合约在使用call操作时需要处理几个关键的安全考虑:
-
回退函数(Fallback Function):代理合约通常需要一个回退函数或接收函数(
receive),用于在调用中没有匹配到任何函数时接收以太币和转发调用。 -
转发返回值:代理合约需要检查
call的返回值,并将任何返回数据转发回原始调用者。 -
错误传播:如果
call操作失败,代理合约通常会使用revert来传播错误,确保调用者知道操作未成功。 -
状态隔离:代理合约自身不应该持有任何状态,所有的状态都应该在逻辑合约中维护,以避免在升级逻辑合约时丢失状态。
代理模式的一个常见例子是 EIP-1967 标准,它定义了一个存储逻辑合约地址的特定存储槽,并推荐了一种标准的代理合约实现方式。此外,还有如 EIP-1538、EIP-1822 和 EIP-2535 等其他代理模式和标准。
代理模式使得智能合约系统更加灵活和可扩展,但同时也引入了额外的 复杂性和潜在的安全风险,因此在设计和实现代理合约时需要格外谨慎。
6. 在 EIP-1559 之前,如何计算以太坊交易的美元成本?
在 EIP-1559 之前,以太坊的交易费用是通过“gas”来计算的,这是一个度量单位,用于衡量执行交易所需的计算工作量。每笔交易的成本(以 ETH 计价)是通过将消耗的 gas 数量乘以 gas 价格(以 gwei 为单位,1 gwei 等于 10^-9 ETH)来计算的。要将这个成本转换为美元,你需要知道在交易发生时 ETH 对美元的汇率。
以下是计算以太坊交易美元成本的步骤:
-
确定交易的 Gas 消耗:每笔交易都有一个
gasUsed的值,这是实际消耗的 gas 数量。 -
找出 Gas 价格:交易发起者会设置一个
gasPrice,这是他们愿意为每单位 gas 支付的价格,以 gwei 为单位。 -
计算 ETH 成本:将
gasUsed乘以gasPrice给出了交易的总成本,以 gwei 为单位。然后,将这个数字转换为 ETH(因为 1 ETH = 10^9 gwei)。 -
转换为美元:最后,需要将 ETH 的成本转换为美元。这需要知道交易发生时 ETH 对美元的兑换率。
例如,如果一笔交易消耗了21,000 gas(这是一个标准的以太坊转账所需的 gas 量),gas 价格是100 gwei,而当时 ETH 对美元的汇率是$2,000,那么交易成本将是:
这个计算给出了交易的大致成本。然而,实际成本可能会因为网络拥堵和交易者愿意支付的 gas 价格而有所不同。EIP-1559 推出后,这个模型被替换为一个包括基础费用和矿工小费的新模型,使得交易费用更加可预测,同时也更加复杂。
7. 在区块链上创建随机数的挑战是什么?
在区块链上创建随机数存在几个挑战,特别是 在去中心化和透明的环境中,如以太坊等公共区块链。以下是主要的挑战:
-
确定性执行:
- 区块链智能合约的执行是确定性的,这意味着给定相同的输入和状态,它们必须产生相同的输出。这是为了确保网络上的所有节点都能独立验证交易和区块。因此,合约本身不能生成真正的随机数,因为这会导致节点之间的不一致。
-
公开透明性:
- 区块链的所有交易和智能合约的状态都是公开的。如果随机数生成依赖于链上信息(如区块哈希值、时间戳等),那么矿工和观察者都可以预测或操纵这些值,从而影响随机数的生成。
-
矿工操纵:
- 矿工负责产生新区块,他们可以决定是否包含特定的交易,并且可以稍微调整区块的属性(例如时间戳)。如果随机数生成依赖于这些属性,矿工可能会有激励来操纵它们以获得对他们有利的结果。
-
攻击者操纵:
- 智能合约中的随机数如果可以被预测,攻击者可以利用这一点来攻击基于随机性的协议,比如赌博游戏或随机分配奖励的系统。
-
跨链交互:
- 有些解决方案可能会考虑使用链外数据源(off-chain)来生成随机数,但这引入了新的信任问题。链外数据源需要通过预言机(oracle)来提供数据,这可能成为中心化的弱点。
为了克服这些挑战,开发人员已经提出了几种解决方案:
- 链外解决方案:使用外部服务(如 Chainlink VRF)来生成随机数,并将其传递给智能合约。
- 经济激励机制:设计激励机制,使得操纵随机数的成本高于操纵所能获得的利益 。
- 承诺方案:参与者提交他们的随机数选择的承诺(例如,哈希值),然后在一个以后的时间点揭示它们。
- 秘密共享和多方计算:多个参与者共同生成随机数,只有在大多数诚实参与者合作的情况下才能计算出最终的随机数。
每种方法都有其优点和局限性。在设计智能合约时,选择哪种随机数生成方法取决于应用的特定需求和可接受的信任模型。
8. 荷兰式拍卖和英式拍卖之间有什么区别?
荷兰式拍卖和英式拍卖是两种不同的拍卖格式,它们在价格设置和竞标过程方面有着本质的区别。
英式拍卖(Ascending-Bid Auction):
- 价格机制:英式拍卖是一种最常见的拍卖形式,也被称作升价拍卖。在英式拍卖中,拍卖从一个相对较低的起始价开始,随着买家相互竞价,价格逐渐上升。
- 出价过程:参与者可以多次出价,每次出价必须高于当前的最高出价。
- 结束条件:拍卖持续进行,直到没有更高的出价出现,最高出价者获得物品。
- 获胜条件:最终,出价最高的参与者赢得拍卖,支付其出价的金额购买物品。
荷兰式拍卖(Descending-Bid Auction):
- 价格机制:荷兰式拍卖是从一个高价开始,随着时间的推移,拍卖价格逐渐降低。
- 出价过程:拍卖官会以一个高于预期成交价的价格开始叫价,然后逐步降低价格,直到有买家接受当前价格。
- 结束条件:第一个愿意接受当前价格的买家即赢得拍卖,拍卖随即结束。
- 获胜条件:获胜者支付的是他们接受的那个价格,而不是更高或更低的价格。
主要区别:
- 价格方向:英式拍卖的价格是逐步升高的,而荷兰式拍卖的价格是逐步降低的。
- 出价次数:在英式拍卖中,买家可以多次出价,而在荷兰式拍卖中,通常第一个出价即结束拍卖。
- 结束速度:荷兰式拍卖可能会很快结束,因为第一个接受当前价格的买家即获得物品,而英式拍卖可能会持续更长时间,因为竞价会持续到没有更高的出价出现。
- 策略:在英式拍卖中,买家可能会采用等待策略,尝试在最后时刻出最高价。在荷兰式拍卖中,买家需要决定何时接受降价中的价格,而这个决策点可能会影响他们是否能赢得拍卖。
两种拍卖形式各有优势和应用场景。荷兰式拍卖因其快速性,常用于商品和金融市场,如花卉拍卖或某些类型的证券拍卖。英式拍卖则常见于艺术品、古董等独特物品的拍卖,参与者愿意支付高于起拍价的价格。
9. ERC20 中的 transfer 和 transferFrom 之间有什么区别?
在 ERC20 代币标准中,transfer和transferFrom函数都用于将代币从一个地址转移到另一个地址,但它们的使用场景和机制有所不同。
transfer 函数:
transfer用于代币持有者将自己的代币直接转移到另一个地址。- 这是一个简单的代币转移操作,只涉及两方:发送者(调用
transfer函数的地址)和接收者。 - 调用
transfer时,只需要指定接收者的地址和转移的代币数量。
函数原型通常如下:
function transfer(address recipient, uint256 amount) public returns (bool);
transferFrom 函数:
transferFrom用于允许一个地址(通常是智能合约或第三方)转移另一个地址授权给它的代币。- 在调用
transferFrom之前,代币的持有者必须首先通过approve函数授权给另一个地址(称为授权者),授权它转移最多特定数量的代币。 transferFrom涉及三方:代币的持有者,被授权的地址(调用transferFrom的地址),以及代币的最终接收者。
函数原型通常如下:
function transferFrom(address sender, address recipient, uint256 amount) public returns (bool);
主要区别:
- 权限:
transfer由代币的持有者使用来直接转移代币,而transferFrom则是由被授权者使用,它允许被授权者将代币从授权者的账户转移到第三方账户。 - 授权:使用
transferFrom之前需要一个授权步骤,即代币持有者必须先调用approve函数授权给第三方一定数量的代币。 - 使用场景:
transfer通常用于简单的代币转移,而transferFrom则常用于更复杂的交互,如代币交易所、代币基于合约的支付逻辑等。
在实际应用中,transfer函数的使用 更为直接和简单,而transferFrom则提供了更多的灵活性,允许构建更复杂的代币交互模式。
10. 对于地址 allowlist,使用映射还是数组更好?为什么?
在智能合约中实现地址 allowlist(白名单)时,使用映射(mapping)和数组(array)各有利弊,选择哪一个取决于你的具体需求和场景。
使用映射 (Mapping):
- 优点:
- 常数时间操作:映射提供了常数时间的查找,这意味着无论列表有多大,检查一个地址是否在 allowlist 中所需的时间都是相同的。
- 简单性:实现起来非常简单,通常只需要一个映射和两个函数(一个用于添加地址,一个用于移除地址)。
- 缺点:
- 枚举困难:映射本身不提供枚举其所有键的方式。如果你需要枚举所有在 allowlist 中的地址,你需要额外的数据结构来跟踪这些地址。
- 无法计数:你不能直接知道映射中有多少个地址,除非你维护一个单独的计数器。
使用数组 (Array):
- 优点:
- 直接枚举:使用数组可以直接枚举所有的元素,这在某些情况下非常有用,例如当你需要对 allowlist 中的地址进行批量操作时。
- 计数简单:数组的长度属性可以直接告 诉你有多少个地址在 allowlist 中。
- 缺点:
- 时间复杂度:在数组中查找一个地址是否存在需要遍历整个数组,这使得操作的时间复杂度为 O(n)。随着数组的增长,这个操作会变得越来越慢。
- 删除成本:从数组中删除一个元素需要移动其他元素来填补空出来的位置,这可能导致高昂的气体费用。
结合使用: 有时候,结合使用映射和数组可以提供同时拥有两者优点的解决方案。你可以使用映射来快速检查地址的存在性,并使用数组来跟踪所有的地址以便枚举。当然,这种方法会增加复杂性和存储成本。
总结:
- 如果你需要高效地检查地址是否在 allowlist 中,并且不需要枚举所有地址,那么映射可能是更好的选择。
- 如果你需要枚举 allowlist 中的地址或者你的列表不会很大,那么数组可能是一个合适的选择。
- 如果你需要两者的优点,可以考虑结合使用映射和数组,但要注意额外的复杂性和成本。
在做出决定时,考虑合约的使用场景和操作的频率是很重要的,因为它们将直接影响交易的气体费用和合约的执行效率。
11. 为什么不应该使用 tx.origin 进行身份验证?
在智能合约中,不建议使用 tx.origin 进行身份验证,原因是安全性问题。tx.origin 与 msg.sender 不同,它指的是交 易的原始发起者,即交易链中最开始的那个地址,而不是当前调用的发起者。
这里是为什么不应该使用 tx.origin 进行身份验证的详细原因:
-
钓鱼攻击(Phishing Attacks): 如果一个合约使用
tx.origin来检查权限,那么恶意合约可以诱骗用户发送交易给它,然后它可以调用另一个合约,那个合约会认为原始用户正在直接调用它。这样,恶意合约可以获得用户在目标合约上的权限。 -
不够灵活: 使用
tx.origin会导致合约无法通过其他合约来调用,因为tx.origin将始终指向外部账户,而不是调用合约的地址。这限制了合约与其他智能合约交互的能力。 -
不符合智能合约模式:
tx.origin不符合现代智能合约开发的模式和最佳实践。智能合约的设计和安全模式通常都是围绕msg.sender构建的,因为它准确地指向了当前的调用者。 -
可能的未来不兼容性: 由于
tx.origin的使用存在安全问题,未来的以太坊改进提案(EIPs)可能会修改或限制tx.origin的使用。依赖于tx.origin的合约可能会因此变得不兼容。 -
误导性:
tx.origin的名字可能会误导开发者认为它是检查交易发起者的正确方式,而实际上msg.sender才是正确地反映当前调用者身份的变量。
出于这些原因,建议总是使用 msg.sender 而不是 tx.origin 来进行身份验证和权限检查。这样可以确保合约的行为符合预期,且不容易受到攻击。
12. 以太坊主要使用什么哈希函 数?
以太坊主要使用的哈希函数是 KECCAK-256,这是一种基于 SHA-3 的加密哈希函数。虽然 SHA-3 是由 KECCAK 算法衍生出来的,但以太坊中所使用的 KECCAK-256 版本在一些细节上与最终的 SHA-3 标准有所不同。
KECCAK-256 在以太坊中有多种用途,包括但不限于:
- 计算交易哈希:每笔交易都通过 KECCAK-256 哈希,以生成一个唯一的交易标识符。
- 生成区块哈希:区块头信息经过哈希处理,形成区块哈希,用于区块链的链接和完整性验证。
- 智能合约地址的生成:当部署一个新的智能合约时,其地址是通过对部署者(发送者)地址和其交易的 nonce 进行 KECCAK-256 哈希处理得到的。
- Merkle Patricia 树:以太坊使用 Merkle Patricia 树(Trie)来存储所有的交易、收据以及账户状态,而这些树的节点也是通过 KECCAK-256 哈希连接起来的。
- EVM 内部操作:以太坊虚拟机(EVM)内部在执行智能合约时,对数据进行哈希处理也会用到 KECCAK-256。
因此,KECCAK-256 是以太坊安全和功能的基石之一,它在整个平台中被广泛使用。
13. 1 个 Ether 相当于 多少个 gwei ?
1 Ether 等于 Gwei
这是因为 "Gwei" 是 "Giga-wei" 的缩写,而 "Giga" 在国际单位制中代表 (十亿)。所以,当你将以太币转换为 Gwei 时,你实际上是将它乘以十亿,或者说 。
14. 1 个 Ether 相当于 多少个 wei ?
1 Ether 等于 Wei。在以太坊中,Wei 是最小的货币单位,而 Ether 是更常用的单位。当你需要从 Ether 转换到 Wei 时,你将每个 Ether 乘以 。
15. assert 和 require 之间有什么区别?
在 Solidity 中,assert和require是用于错误处理的两个不同的函数,它们在条件不满足时会恢复状态并撤销所有状态更改。它们之间的主要区别在于它们各自的用途和它们如何处理气体费用(gas)。
-
require:- 通常用于检查函数的输入条件、合约状态变量的条件,或者在执行状态转换之前对环境的条件进行响应。
- 如果
require条件失败,它将恢复所有状态更改,撤销所有对状态和以太币余额的修改,并退还剩余的气体给调用者。 require函数允许你指定一个错误信息,这个错误信息在条件失败时将被返回给调用者。
-
assert:- 通常用于检查代码不应该达到的状态,也就是说,用来检测程序内部错 误和不一致性。
- 如果
assert条件失败,它同样会恢复所有状态更改,但与require不同的是,它不会退还剩余的气体。这是因为assert失败通常表明存在一个更严重的错误,如算术运算溢出或者不变量的破坏。 assert不允许指定错误信息,它会消耗所有提供的气体,并返回一个固定的错误类型。
使用示例:
- 使用
require来检查函数的参数是否有效,或者验证合约的状态是否允许某个操作。 - 使用
assert来检查代码中的逻辑错误,例如,在执行某些操作后,检查不变量是否仍然保持。
在实践中,require被用得更频繁,因为它涉及到与外部交互和条件检查,而assert则用于表示那些几乎不可能发生的情况,如果发生了,可能意味着合约中存在严重的错误。
16. 什么是闪电贷?
闪电贷(Flash Loans)是去中心化金融(DeFi)领域的一种创新贷款产品,它允许用户在没有事先资本的情况下借入大量资产,但有一个前提条件,那就是在同一个区块交易中借入和归还这些资产。这意味着,用户必须在一个区块的交易开始时借入资金,并在同一个区块的交易结束前归还资金,通常还要支付一定的费用。
闪电贷的特点包括:
-
无抵押:用户不需要提供抵押品就可以借款,这在传统贷款和大多数 DeFi 贷款产品中是不可能的。
-
短期:借款和还款必须在同一个区块内完成,通常在几秒钟内。
-
大额度:用户可以借入任何数量的资金,只要市场上有足够的流动性。
-
风险:如果用户不能在一个区块内还款,整个交易将被撤销,就像从未发生过一样。这意味着提供闪电贷的平台不会承担违约风险。
闪电贷的用途:
- 套利:利用不同交易平台之间的价格差异来盈利。
- 抵押品互换:更换在 DeFi 协议中的抵押品而不必关闭债务头寸。
- 自我清算:清算你在 DeFi 借贷平台上的贷款头寸,如果你的抵押品价值下跌而接近被清算线。
- 投票权:借入代币以参与治理投票,然后在投票结束后立即归还。
闪电贷的实施通常依赖智能合约和自动化的执行,而且它们在 DeFi 生态系统中是可能的,因为所有的资产和交易都是透明的,且可以通过智能合约编程和执行。这种类型的贷款由于其无抵押和高风险的性质,主要被高级用户和开发者用于复杂的金融策略,而不是普通用户。
17. 什么是检查效果( check-effects )模式?
检查效果(Check-Effects-Interactions)模式是智能合约编程中的一种最佳实践,特别是在以太坊的 Solidity 语言中。这个模式的目的是减少智能合约中的安全漏洞,特别是防止重入攻击(Reentrancy Attack)。
重入攻击是智能合约最著名的安全隐患之一,最著名的例子是 2016 年的 DAO 攻击。在重入攻击中,攻击者可以在合约完成其所有内部状态更新之前,多次调用同一函数。
检查效果模式建议将智能合约函数的逻辑分为三个阶段:
-
检查(Checks): 首先,验证所有的前提条件是否满足。这包括权限检查(确保调用者有权执行该操作)和参数检查(确保输入参数在合理范围内)。
-
效果(Effects): 然后,在进行任何外部调用之前,先更新合约的状态。这是为了防止在合约状态更新之前发生的重入可能导致的不一致状态。
-
交互(Interactions): 最后,执行所有的外部调用,如向其他合约发送以太币或调用其他合约的函数。因为此时合约的状态已经更新,即使发生重入,也不会导致状态不一致或其他漏洞。
遵循这个模式可以帮助开发者编写更安全的智能合约,因为它强制将可能引发重入的外部调用移至所有关键的状态变更之后。这样,即使攻击者尝试进行重入,他们也无法因为合约状态尚未更新而利用合约的漏洞。
下面是一个简化的 Solidity 代码示例,演示了检查效果模式:
pragma solidity ^0.8.0;
contract SafeContract {
mapping(address => uint) public balances;
// 检查效果模式的函数
function withdraw(uint _amount) public {
// 检查(Checks)
require(balances[msg.sender] >= _amount, "Insufficient balance");
// 效果(Effects)
balances[msg.sender] -= _amount;
// 交互(Interactions)
(bool sent, ) = msg.sender.call{value: _amount}("");
require(sent, "Failed to send Ether");
}
// 存款函数,用于增加合约余额
function deposit() public payable {
balances[msg.sender] += msg.value;
}
}
在这个例子中,withdraw 函数首先检查调用者是否有足够的余额(检查),然后更新内部余额映射(效果),最后发送以太币(交互)。这个顺序确保了即使在发送以太币的过程中调用者再次调用withdraw函数,由于余额已经更新,重入也不会导致额外的以太币被不当提取。
在您提到的例子中,如果合约的balances[msg.sender]为 10,并且调用者连续两次调用withdraw函数,每次提取 1 个单位(假设单 位是以太币),那么按照正常逻辑,balances[msg.sender]应该是减少 2 个单位,从 10 变到 8。
然而,您提到的情况似乎是指在没有防范措施的情况下,可能发生的重入攻击。在这种情况下,如果合约的函数没有正确地遵循检查效果模式,并且在发送以太币之前没有正确地更新内部状态,那么攻击者可能会在合约发送以太币给他们之后,立即再次调用withdraw函数。这样,他们可以在合约状态更新之前多次提取资金,这可能会导致更多的资金被提取,超过了他们原本应该能够提取的数量。
但是在您给出的代码示例中,已经正确地遵循了检查效果模式:
-
检查(Checks):
require(balances[msg.sender] >= _amount, "Insufficient balance");确保用户的余额大于或等于他们想要提取的金额。 -
效果(Effects):
balances[msg.sender] -= _amount;在任何外部调用之前,更新了用户的余额。 -
交互(Interactions):
(bool sent, ) = msg.sender.call{value: _amount}("");最后,发送以太币给调用者。
在这个流程中,即使攻击者在msg.sender.call{value: _amount}("");调用期间重新进入合约,由于余额已经被更新,他们不能提取超过他们余额的资金。因此,在这种情况下,如果攻击者尝试两次提取,他们的余额将正确地从 10 减少到 8。
如果您指的是在没有防范措施的情况下,确实可能会出现您描述的问题,但是按照您提供的代码示例,合约是安全的,不会出现错误地只减少 1 个单位的情况。
18. 运行独立验证节点所需的最小以太数 量是多少?
截至我最后更新的信息(2023 年 4 月),运行一个以太坊 2.0 的验证节点(也称为质押节点)需要质押至少 32 ETH。这是因为以太坊 2.0 转向了权益证明(Proof of Stake,PoS)共识机制,这要求验证节点的运营者锁定一定数量的 ETH 作为“质押”,以此参与区块的验证和创建过程。
如果你没有足够的 ETH 来独立运行一个验证节点,你可以选择加入质押池。这些池允许多个贡献者合并他们的 ETH,共同满足 32 ETH 的要求,并共享任何产生的奖励。
请注意,这些要求和数字可能会随着时间和以太坊协议的变化而变化,所以最好是查看最新的以太坊文档或社区更新来获取最新信息。
19. fallback 和 receive 之间有什么区别?
在 Solidity 中,fallback 和 receive 函数都是智能合约用来接收以太的特殊函数。
receive函数是当合约仅接收到以太(没有附加数据)时自动调用的。fallback函数是当调用合约中不存在的函数,或者合约接收到带有额外数据的以太时调用的。
如果receive存在,它会在收到纯以太交易时优先被调用。如果receive不存在或交易包含数据,fallback就会 被调用。
20. 什么是重入?
重入(Reentrancy)是智能合约中的一个著名的安全漏洞,特别是在以太坊和其他基于 EVM(以太坊虚拟机)的区块链上。重入攻击发生在一个合约调用另一个合约的函数,并且在第二个合约执行过程中,第二个合约再次调用第一个合约的函数,这可能导致意外的行为,如重复提取资金。
这通常发生在以下的场景中:
- 合约 A 调用合约 B 的一个函数来进行某种资金交易。
- 合约 B 在接收到资金后,通过某种机制(如回调函数)再次调用合约 A 的函数。
- 如果合约 A 在更新其内部状态(例如,记录已经发送资金)之前响应合约 B 的回调,那么合约 B 可以利用这个时机再次从合约 A 提取资金。
- 这个过程可以重复多次,直到合约 A 的资金被完全提取,这通常是由于合约 A 没有正确地管理其内部状态,或者没有实施适当的锁定机制。
重入攻击的经典案例是“The DAO”事件,这是一个在 2016 年导致大量以太币被盗的安全漏洞。为了防止重入攻击,开发者应该采取一些措施,如:
- 使用互斥锁(Mutex):在执行外部调用前,确保合约的关键部分不可再入。
- 检查-效果-交互模式(Checks-Effects-Interactions):首先进行所有的检查(例如余额检查),然后更新合约状态,最后进行外部调用或交互。
- 使用可重入性守卫:如 OpenZeppelin 的
ReentrancyGuard合约,它提供了一个简单的修饰符来阻止重入。
理解和预防重入攻击是智能合约开发中的一个重要方面 ,对于保障区块链系统的安全性至关重要。
21. 上海升级后,每个区块的 gas 限制是多少?
在以太坊上,每个区块的 gas limit 是由矿工决定的。在上海升级后,每个区块的 gas limit 已经从 15,000,000 增加到了 30,000,000
22. 什么阻止无限循环永远运行?
如果理解成:是什么不让无限循环永远执行的话?答案就很明显,方法会受到 gas 费限制,栈深度的限制,所以循环不会一直永远运行,会报 out of gas 错误。 如果题目理解成:做什么可以阻止无限循环。那就是代码实现,在循坏中使用计数器:require 判断是否达到满足中断循坏次数。(感谢评论区指出错误)
23. tx.origin 和 msg.sender 之间有什么区别?
tx.orgin 是这笔交易的发起者,msg.sender 是前一个调用发起者,如交易流程:张三发起交易-合约 A-合约 B,在合约 B 中 msg.sender = 合约 A,tx.orgin = 张三
24. 如何向没有 payable 函数、receive 或回退的合约发送以太?
在以太坊上,向智能合约发送 ETH 通常需要合约有一个标记为 payable 的函数,或者至少有一个 receive() 或 fallback() 函数,也被标记为 payable。这是因为智能合约需要明确地声明它们愿意接收 ETH,否则,合约在设计上是拒绝接收 ETH 的。
然而,如果一个合约没有任何 payable 函数、receive() 或 fallback() 函数,它在正常情况下是不能直接接收 ETH 的。但是,仍然有一些非标准的方法可以使 ETH 发送到这样的合约:
-
自毁(Self-Destruct): 自毁(
selfdestruct)操作可以将合约的余额发送到任何地址,包括没有payable函数的合约地址。如果你控制一个合约,你可以调用selfdestruct并将没有payable函数的合约地址作为目标,这样 ETH 就会被发送到那个合约地址。但请注意,这是一个不可逆转的操作,一旦执行,合约代码和存储都将从区块链上移除。 -
在合约创建时发送 ETH: 在创建合约的交易中,你可以发送 ETH 到合约地址。这个操作不要求合约有
payable函数。但这只适用于合约创建过程,一旦合约被创建,就不能再用这种方法发送 ETH 了。 -
预发送 ETH: 在合约被创建之前,如果你知道合约将要部署的地址(可以通过创建者的地址和 nonce 计算得出),你可以向这个未来的地址发送 ETH。然后,当合约被部署到该地址时,它将拥有这些 ETH。这种方法很少使用,因为它涉及到对以太坊地址生成方式的深入理解。
-
通过合约漏洞: 如果合约中存在漏洞,例如某些函数意外地允许发送 ETH 或者状态变量可以被不当修改导致 ETH 的接收,这可能会被利用来发送 ETH 到合约。这种方法是危险且不道德的,通常只在攻击场景中出现。
在所有情况下,如果合约没有办法提取或使用发送到它的 ETH,那么这些 ETH 可能会被永久锁定。因此,在尝试任何非标准的方法之前,你应该非常小心,并确保你完全理解可能的后果。
25. view 和 pure 之间有什么区别?
view 和 pure 都是函数的修饰符,view 可以访问合约中的状态变量,不能修改;pure 不能访问也不能修改状态变量
26. ERC721 中的 transferFrom 和 safeTransferFrom 之间有什么区别?
在 ERC-721 标准中,transferFrom和safeTransferFrom函数都用于将一个 NFT(非同质化代币)从一个地址转移到另一个地址。然而,两者之间有一个重要的区别,主要涉及到安全性。
-
transferFrom:
transferFrom函数用于转移 NFT 的所有权。它只需要知道代币的当前所有者、新的所有者以及代币的 ID。调用此函数时,假设调用者已经得到了授权,或者是当前代币的所有者。transferFrom不检查接收者是否能够接收 NFT,也就是说,它不确认目标地址是否是一个知道如何处理 ERC-721 代币的智能合约。如果 NFT 被发送到了一个无法处理它的合约地址,那么代币可能会永久地被锁定。 -
safeTransferFrom:
safeTransferFrom函数也用于转移 NFT 的所有权,但它增加了一个安全检查。当 NFT 被发送到一个合约地址时,safeTransferFrom会检查该合约是否实现了onERC721Received函数,这是一种特殊的函数,用于确认合约知道如何处理 ERC-721 代币。如果目标合约没有实现onERC721Received,或者没有正确地响应,那么交易将失败并且回滚,这样可以防止 NFT 被意外地发送到不支持它们的合约中。
简而言之,safeTransferFrom比transferFrom更安全,因为它确保了 NFT 不会被无意中发送到无法处理它的合约中。这就是为什么 ERC-721 标准建议在可能将代币发送到合约地址的情况下使用safeTransferFrom。实际上,大多数情况下,都应该使用safeTransferFrom以确保交易的安全性。
27. 如何将 ERC1155 代币转换为非同质化代币?
ERC1155 代币是一种可替代和不可替代的代币,可以在同一合约中管理多个资产。ERC721 代币是一种非可替代代币,每个代币都是唯一的。因此,将 ERC1155 代币转换为 ERC721 代币需要将每个 ERC1155 代币转换为一个唯一的 ERC721 代币。 要将 ERC1155 代币转换为 ERC721 代币,您需要执行以下步骤:
- 创建一个新的 ERC721 合约。
- 为每个 ERC1155 代币创建一个新的 ERC721 代币。
- 将 ERC1155 代币的所有权转移到新的 ERC721 代币。
- 销毁原始 ERC1155 代币
28. 访问控制是什么,为什么重要?
访问控制是一种重要的机制,用于限制对智能合约的访问。通过使用访问控制,您可以确保只有经过授权的用户才能执行特定操作或访问敏感信息。这可以帮助保护您的智能合约免受未经授权的访问和攻击。 Solidity 提供了几种访问控制修饰符,例如 public、private、internal 和 external。这些修饰符用于控制函数和状态变量的可见性和访问权限。
29. 修饰符(modifier)的作用是什么?
modifier 是一种函数修饰符,用于声明一个函数修改器。函数修改器的作用与 Spring 中的切面功能很相似,当它作用于一个函数上,可以在函数执行前或后(依赖于具体实现)预先执行 modifier 中的逻辑,以增强其功能。使用 modifier 可以将一些通用的操作提取出来,提高编码效率,降低代码耦合性。 modifier 可以用于控制函数的访问权限和行为。例如,如果您希望只有合约的所有者才能访问某个函数,您可以将该函数标记为 private,并使用 modifier 来检查调用者是否为合约的所有者。如果调用者不是合约的所有者,则函数将停止执行并回滚所有更改。这可以帮助保护合约免受未经授权的访问和攻击。
30. uint256 可以存储的最大值是多少?
最大 2^256-1 。为什么减 1?0 占用了一个空间,256 位的表示的数是从 000...00 到 111...11 。例如:如果有 uint2 , 那么 2^2 可存储的数可以是 00 = 0 ,01 = 1,10 = 2,11 = 3,这里最大是 3,也就是 2^-1,而不是 4。
31. 什么是浮动利率和固定利率?
固定利率是指在贷款期间内,贷款利率保持不变。而浮动利率是指贷款利率会随着市场利率的变化而变化。浮动利率通常基于某个基准利率,例如央行基准利率或 LIBOR(伦敦银行间同业拆借利率)。如果基准利率上升,贷款利率也会上升,反之亦然。 send 函数的 gas 限制也为 2300 gas,但它返回一个布尔值,指示转账是否成功。如果转账失败,它将返回 false。但是,如果接收方合约没有实现 fallback 函数,或者 fallback 函数消耗的 gas 超过了 2300,那么转账将失败并回滚所有更改。
参考链接
- https://learnblockchain.cn/article/7076
- 简单题 https://learnblockchain.cn/article/7257
- 中等难度 https://learnblockchain.cn/article/7260
- 有难度题 https://learnblockchain.cn/article/7264
- 高难度题 https://learnblockchain.cn/article/7268
- gas 优化 https://learnblockchain.cn/article/7275
- 以太坊的 EIP 提案 https://learnblockchain.cn/article/7293
- https://decert.me/tutorials