您正在查看: Ethereum 分类下的文章

深入理解EVM操作码,让你写出更好的智能合约

你的一些编程“好习惯”反而会让你写出低效的智能合约。对于普通编程语言而言,计算机做运算和改变程序的状态顶多只是费点电或者费点时间,但对于 EVM 兼容类的编程语言(例如 Solidity 和 Vyper),执行这些操作都是费钱的!这些花费的形式是区块链的原生货币(如以太坊的 ETH,Avalanche 的 AVAX 等等...),想象成你是在用原生货币购买计算资源。

用于购买计算、状态转移还有存储空间的开销被称做 燃料(下文统称 gas )。 gas 的作用是确定交易的优先级, 同时形成一种能抵御【女巫攻击】(Sybil resistance)的机制 ,而且还能防止【停机问题】(halting problem)引起的攻击。

欢迎阅读我的文章 Solidity 基础 去了解 gas 的方方面面

这些非典型的开销导致经典的软件设计模式在合约编程语言中看起来既低效又奇怪。如果想要识别这些模式并理解他们导致效率变高/低的原因,你必须首先对以太坊虚拟机(即 EVM)有一个基本的了解。

什么是EVM?

如果你已经熟悉 EVM,请随时跳到下个部分: 什么是 EVM 操作码?

任何一个区块链都是一个基于交易的 状态机。 区块链递增地执行交易,交易完成后就变成新状态。因此,区块链上的每笔交易都是一次状态转换。

简单的区块链,如比特币,本身只支持简单的交易传输。相比之下,可以运行智能合约的链,如以太坊,实现了两种类型的账户,即外部账户和智能合约账户,所以支持复杂的逻辑。

外部账户由用户通过私钥控制,不包含代码;而只能合约账户仅受其关联的代码控制。EVM 代码以字节码的形式存储在虚拟 ROM 中。

EVM 负责区块链上所有交易的执行和处理。它是一个栈机器,栈上的每个元素长度都是 256 位或 32 字节。EVM 嵌在每个以太坊节点中,负责执行合约的字节码。

EVM 把数据保存在 存储(Storage) 和 内存(Memory) 中。存储(Storage)用于永久存储数据,而内存(Memory)仅在函数调用期间保存数据。还有一个地方保存了函数参数,叫做调用数据(calldata),这种存储方式有点像内存,不同的是不可以修改这类数据。

在 Preethi Kasireddy 的文章中了解有关以太坊和 EVM 的更多信息 Ethereum 是如何工作的?。

智能合约是用高级语言编写的,例如 Solidity、Vyper 或 Yul,随后通过编译器编译成 EVM 字节码。但是,有时直接在代码中使用字节码会更高效(省gas)。

EVM 字节码以十六进制编写。它是一种虚拟机能够解释的语言。这有点像 CPU 只能解释机器代码。

什么是 EVM 操作码?

所有以太坊字节码都可以分解为一系列操作数和操作码。操作码是一些预定义的操作指令,EVM 识别后能够执行这个操作。例如,ADD 操作码在 EVM 字节码中表示为 0x01。它从栈中删除两个元素并把结果压入栈中。

从堆栈中移除和压入堆栈的元素数量取决于操作码。例如,PUSH 操作码有 32 个:PUSH1 到 PUSH32。 PUSH 在栈上 添加一个 字节元素,元素的大小可以从 0 到 32 字节。它不会从栈中删除元素。作为对比, 操作码 ADDMOD 表示 [模加法运算](https://libraryguides.centennialcollege.ca/c.php?g=717548&p=5121840#:~:text=Properties of addition in modular,%2B d ( mod N ) .) ,它从栈中删除3个元素然后压入模加结果。请注意,PUSH 操作码是唯一带有操作数的操作码。

每个操作码都占一个字节,并且操作成本有大有小。操作码的操作成本是固定的或由公式算出来。例如,ADD 操作码固定需要3 gas。而将数据保存在存储中的操作码 SSTORE ,当把值从0设置为非0时消耗 20,000 gas,当把值改为0或保持为0不变时消耗 5000 gas。

SSTORE 的开销实际上会其他变化,具体取决于是否已访问过这个值。可以在这里找到有关 SSTORE 和 SLOAD 开销的完整详细信息:

为什么了解 EVM 操作码很重要?

想要降低 gas 开销,了解 EVM 操作码极其重要,这也会降低你的终端用户的成本。由于不同的 EVM 操作码的成本是不同的,因此虽然实现了相同结果,但不同的编码方式可能会导致更高的开销。了解哪些操作码是比较昂贵的,可以帮助你最大程度地减少甚至避免使用它们。你可以查看 以太坊文档 以获取 EVM 操作码及其相关 gas 开销的列表。

下面是一些考虑了 EVM 操作码开销的反直觉设计模式的具体示例:
用乘法而不是指数: MUL vs EXP
MUL 操作码花费 5 gas 用于执行乘法。例如,10 * 10 背后的算术将花费 5 gas。

EXP 操作码用于求幂,其 gas 消耗由公式决定:如果指数为零,则消耗10 gas。但是,如果指数大于零,则需要 10 gas 加上指数字节数的 50 倍。

一个字节是 8 位,一个字节可以表示 0 到 2⁸-1 之间的值(即0-255),两个字节可以表示 2⁸ 到 2¹⁶-1 之间的值,以此类推。因此,例如求 10¹⁸ 将花费 10 + 50 1 = 60 gas,而求 10³⁰⁰ 将花费 10 + 50 2 = 160 gas,因为来表示 18 需要一个字节,表示 300 需要两个字节。

从上面可以清楚地看出,在某些时候你应该使用乘法而不是求幂。下面一个具体的例子:

contract squareExample {
uint256 x;
constructor (uint256 _x) {
   x = _x;
 }
function inefficcientSquare() external {
   x = x**2;
 }
function efficcientSquare() external {
     x = x * x;
 }
}

inefficientSquare 和 eficcientSquare 两个方法都把状态变量 x 改为 x 的平方。然而,inefficientSquare 的算术开销为 10 + 1 * 50 = 60 gas,而 efficientSquare 的算术开销为 5 gas。

由于上述算术开销之外的原因,inefficientSquare 的 gas 费用平均比 efficientSquare 多 200 左右。

缓存数据:SLOAD & MLOAD

众所周知,缓存数据可以大规模地提升更好的性能。同样,在 EVM 上使用缓存也极端重要,即使只有少量操作,也会明显节省 gas。

SLOAD 和 MLOAD 两个操作码用于从存储和内存中加载数据。MLOAD 成本固定 3 gas,而 SLOAD 的成本由一个公式决定:SLOAD 在交易过程中第一次访问一个值需要花费 2100 gas,之后每次访问需要花费 100 gas。这意味着从内存加载数据比从存储加载数据便宜 97% 以上。

下面是一些节省潜在 gas 的示例代码:

contract storageExample {
uint256 sumOfArray;
function inefficcientSum(uint256 [] memory _array) public {
        for(uint256 i; i < _array.length; i++) {
            sumOfArray += _array[i];
        }
} 
function efficcientSum(uint256 [] memory _array) public {

   uint256 tempVar;
   for(uint256 i; i < _array.length; i++) {
            tempVar += _array[i];
        }
   sumOfArray = tempVar;
} 
}

合约 storageExample 有两个函数: inefficientSum 和 efficientSum

这两个函数都将 _array 作为参数,这是一个无符号整型数组。他们都会把合约的状态变量 sumOfArray 设置为 _array 中所有元素的总和。

inefficcientSum 使用状态变量进行计算。请牢记,状态变量(例如 sumOfArray)保存在 存储 中。

efficcientSum 在内存中创建一个临时变量 tempVar,用于计算 _array 中值的总和。然后将 tempVar 赋值给 sumOfArray。

当传入的数组仅包含 10 个无符号整数时, efficientSum的 gas 效率比 inefficcientSum 高 50% 以上。

它们的效率随着计算次数的增加而增加:当传入 100 个无符号整数的数组时,eficcientSum 比 inefficcientSum 的 gas 效率高 300% 以上。

避免使用面向对象编程模型:CREATE 操作码

CREATE 操作码用于创建包含关联代码的新帐户(即智能合约)。它花费至少32,000 gas,是 EVM 上最昂贵的操作码。

最好尽可能减少使用的智能合约数量。这与典型的面向对象编程不同,在典型的面向对象编程中,为了可复用性和清晰性,鼓励定义多个类。

这是一个具体的例子:

下面是一段使用面向对象方法创建“vault”的代码。每个“vault”都包含一个 uint256 变量,并在构造函数中初始化:

contract Vault {
    uint256 private x; 
    constructor(uint256 _x) { x = _x;}
    function getValue() external view returns (uint256) {return x;}
}
//  Vault 结束
interface IVault {
    function getValue() external view returns (uint256);
} // IVault 结束

contract InefficcientVaults {
    address[] public factory;
    constructor() {}

    function createVault(uint256 _x) external {
        address _vaultAddress = address(new Vault(_x)); 
        factory.push(_vaultAddress);
    }

    function getVaultValue(uint256 vaultId) external view returns (uint256) {
        address _vaultAddress = factory[vaultId];
        IVault _vault = IVault(_vaultAddress);
        return _vault.getValue();
    }
} // InefficcientVaults 结束

每次调用 createVault() 时,都会创建一个新的 Vault 智能合约。存储在 Vault 中的值由传递给 createVault() 的参数决定。然后将新合约的地址存储在数组 factory 中。

这是另一段实现相同功能的代码,但用映射代替了创建:

contract EfficcientVaults {
  // 映射:vaultId => vaultValue
  mapping (uint256 => uint256) public vaultIdToVaultValue;

  // 下一个 vault 的 id
  uint256 nextVaultId;

  function createVault(uint256 _x) external {
      vaultIdToVaultValue[nextVaultId] = _x;
      nextVaultId++;
  }

  function getVaultValue(uint256 vaultId) external view returns (uint256) {
      return vaultIdToVaultValue[vaultId];
  }
} // EfficcientVaults 结束

每次调用 createVault() 时,参数都存储在一个映射中, 映射的 ID 由状态变量 nextVaultId 确定,而 nextVaultId 在每次调用 createVault() 时递增。

这种实现上的差异导致 gas 成本大幅降低。

EfficcientVaults 的 createVault() 与 IneficcientVaults 相比,效率提高了 61%,消耗的 gas 减少了约 76,300。

应该注意的是,在某些情况下在合约中创建新合约是可取的,并且通常是为了不可变性和效率。随着合约的大小增加,与合约的所有交互的交易成本也将增加。 因此,如果你希望在链上存储大量数据,最好通过多个单独的合约分离这些数据。除此之外,应避免创建新合约。

存储数据:SSTORE

SSTORE 是将数据保存到存储的 EVM 操作码。一般而言,当将存储值从零设置为非零时,SSTORE 花费 20,000 gas,当存储值设置为零时,SSTORE 花费 5000 gas。

由于这种成本的存在,在链上存储数据效率低下且成本高昂,应尽可能避免。

这种方法在 NFT 中最为常见。开发人员将 NFT 的元数据(图像、属性等)存储在去中心化存储网络(如 Arweave 或 IPFS)上,而不是将其存储在链上。唯一保存在链上的数据是一条指向元数据的链接。可通过所有 ERC721 合约内置的 tokenURI() 函数获得此链接。

tokenURI() 函数的标准实现。 (来源:OpenZeppelin)

例如无聊猿Bored Ape Yacht Club smart contract。 调用 tokenURI( ) 函数,传入 tokenId: 0, 函数返回以下链接: ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/0


如果点击链接,你将看到 BAYC #0 元数据的 JSON 文件:
这些数据在OpenSea上很容易验证: OpenSea:

还应注意,由于存储成本,某些数据结构在 EVM 中根本不可行。例如,使用邻接矩阵表示图(a graph using an adjacency matrix) 是完全不可行的,因为它的空间复杂度是 O(V²) 。

以上所有代码都可以在我的Github上找到

感谢你阅读,希望你喜欢这篇文章!

如果有机会,我愿意介绍更多的 gas 优化和细微差别。要了解更多信息,我建议使用以下资源:

  • 变量压缩打包大法 和 内存数组优化 作者: Franz Volland
  • Solidity gas 优化技巧 和 Solidity 节省 gas 和字节码大小的魔法 作者: Mudit Gupta
  • EVM: 从 Solidity 到字节码, 内存和存储 作者: Ethereum 工程小组
  • 以太坊黄皮书

转载自:https://learnblockchain.cn/article/5137

Solidity 优化 - 隐藏的 Gas 成本

本文将研究以太坊虚拟机(EVM)的内部工作,以说明如何 "利用 "EVM的特殊特性,为用户最小化solidity智能合约的执行成本。社区发布许多关于 solidity开发者可以利用的知识来设计和开发更安全、更节省Gas的智能合约。

本周我们看一下两种不同类型的优化,这些优化相对简单,可以应用于任何代码库,与我们为客户审计的大多数智能合约有关。第一个优化适用于所有版本的 Solidity。本文讨论的第二个优化只对pragma版本0.8.0以上有效。然而,让我们首先在高层次上阐明如何评估任何EVM指令的成本。

评估EVM指令成本

最核心的是,在EVM区块链上为交易引入 "Gas 成本"和为组装区块引入 "Gas限制 "的理由是:

  1. 引入额外的收入流,以激励stakers(以前的矿工)确保和验证网络,以及
  2. 作为保护措施,防止拒绝服务(DoS)攻击,通过Gas上限禁止执行计算昂贵的任务(例如,具有显著限制的 "for "循环,否则会延迟区块创建率的中位数)

为了在区块构建者补偿和有竞争力的计算系统之间取得余额,EVM区块链根据网络行为者之间的带宽需求动态地调整其区块Gas限制。交易Gas成本通常发展缓慢,围绕着一个简单的基础;执行交易的计算成本。在中心化和/或传统的计算环境中,这可能看起来很简单。然而,在区块链生态系统中,由于区块链账本的状态变化在验证去中心化网络上执行的指令的节点网络中传播的方式,它有很大不同。

在这篇文章中,我们说明了 "内存" 的隐性成本如何抬高了EVM区块链上其他直接交易类型的成本,以及开发者如何优化他们的dapps以减少其Gas足迹。

EVM本地变量的隐藏成本

当声明一个作为语句结果的局部变量时,会产生一个隐藏的Gas成本,它与我们声明的局部变量所需的 内存 量成比例。当从存储空间读取变量(SLOAD),将它们存储到局部变量(参考 MSTORE 内存扩展 增加了隐藏成本),以及在每次利用时读取它们(MLOAD)时,这种额外成本通常会被抵消。

在处理EVM的原始指令时,情况就不是这样了。事实上,区块链上的每笔交易都包含一组不可避免的数据。因此,数据集通过原始的EVM指令暴露给所有智能合约,这些指令消耗的Gas 非常小。这是由于它们不需要额外的内存来读取特殊的数据槽,因为它们已经作为EVM的区块创建工作流程的一部分在内存中加载。

这些指令集很重要,他们围绕着交易的上下文数据,如msg.sender,block.timestamp,等等。因此,下面的合约实现事实上是低效的。

Context 合约是OpenZeppelin引入的一个实现,目的是简化合约的开发过程,可以很容易地升级到元交易兼容的合约。然而,到目前为止,它大多被滥用,并导致各种协议(包括Aave V2和Aave V3)的Gas增加到不可忽略的程度,这是Aave V3的 "IncentivizedERC20 "实现的具体例子:

在上述函数中,gas 成本包含:_msgSender实现的 msg.sender Gas成本(操作码 CALLER: 2 gas),以及_msgSender()调用本身(操作码 JUMP: 2gas以及返回变量的内存分配)两次。通过优化上述片段,我们可以将指令的Gas成本降低一半:

虽然这种优化本身可能微不足道,但在整个代码库中应用时,它将带来切实的节省。

Solidity数学上的隐藏成本

隐性Gas成本不仅限于EVM。开发人员需要认识到,Solidity语言本身在其最新版本semver 8中引入了一些隐性成本,Solidity默认执行安全算术。鉴于很多应用程序已经对不安全的算术操作进行了安全检查,作为其错误处理工作流程的一部分,内置的安全算术检查变得多余了,因此会产生多余的Gas增加。

值得庆幸的是,Solidity还引入了一种新的代码块声明风格,指示编译器不安全地执行算术操作。unchecked代码块。只要操作被周围的语句和/或条件保证安全执行,就可以巧妙地利用这些代码块来大大减少特定合约的Gas成本。作为一个例子,让我们看一下复合CToken实现的_reduceReservesFresh函数的这一段:


在条件 reduceAmount > totalReserves 被评估为 false 之后,totalReservesNew 的计算和分配被执行。这意味着执行环境已经保证了 "totalReserves >= reduceAmount "这一特性,因此 "totalReservesNew "的计算可以在一个unchecked 的代码块中进行,因为它被保证能够正常执行。经过优化,上述代码块应该类似于这样:

另一种避免内置安全算术产生额外Gas的方法是在增量操作(++和--)期间使用unchecked。通常在for循环和任何0.8.X后的版本中进行操作都有此问题。每一次增量操作都会进行边界检查,当它们完全是多余的。下面提供一个非常简单的例子来说明这一点:

由于Solidity的固有限制,bar.length被保证适合于uint256变量,这意味着对i变量的每个循环执行安全增量将是多余的。为了优化这样的代码块,我们把增量移到 unchecked 的代码块的末尾:

结论

EVM是一个内在复杂的机器,因此已经开发了多种工具来帮助开发者使用高级语言(如Solidity)在其系统中创建解决方案。然而,在Solidity编译器的自由下进行的简化,通常没有很好地转达给开发者社区,因此,程序员最终创造了低效的程序。

转载自:https://learnblockchain.cn/article/5141

Foundry教程|如何调试和部署Solidity智能合约

Foundry是一个Solidity框架,用于构建、测试、模糊、调试和部署Solidity智能合约。在这个Foundry教程中,我们将介绍以下内容。

  • Foundry简介 [视频]
  • Foundry入门
  • 使用Foundry的Hardhat
  • 使用 Solidity 测试
  • Foundry作弊代码
  • 部署和使用合约

Foundry简介

我制作了一个[视频],你可以在YouTube上观看:https://youtu.be/VhaP9kYvlOA
智能合约通常是用Solidity编写的,但使用Javascript框架(如Truffle或Hardhat)进行测试和部署。Foundry提供了一套在Rust中构建的工具,允许区块链开发者在Solidity中编写测试,并通过命令行部署和与合约交互。

为什么用Foundry?

  • 在solidity中编写单元测试,而不是Javascript
  • 更快的编译和测试
  • 内置的模糊测试
  • Gas优化工具
  • 支持主网分叉
  • Etherscan 代码验证
  • 硬件钱包兼容
  • Solidity脚本
  • 通过作弊代码操纵区块链状态

开始使用Foundry

安装说明

为了开始使用,我们需要安装foundry包,它需要rust。以下是linux、mac、windows和docker的命令。

Linux/Mac:

curl -L https://foundry.paradigm.xyz | bash;
foundryup

Windows: (需要 Rust,从 https://rustup.rs/ 安装)

cargo install --git https://github.com/foundry-rs/foundry --bins --locked

Docker:

docker pull ghcr.io/foundry-rs/foundry:latest

使用foundry的第一步

Foundry软件包带有两个主要的命令行功能:

  • forge - 建立编译测试本地智能合约
  • cast - 使用已部署的智能合约执行链上交易

如果想从Github上克隆一个repo,我们可以使用forge命令。

forge install jamesbachini/myVault -hh

这里我们使用的是Github用户名和版本库名称,加上-hh 参数,用于迁移Hardhat版本库。

我们也可以用 "myrepo" 初始化一个新的版本库。

forge init myrepo

然后就可以继续编译和测试智能合约了

forge build
forge test


注意测试是如何通过的,还得到了测试交易的Gas成本

如何使用 Hardhat 设置 Foundry

假设我们已经按照上面的说明安装了 Foundry,我们就可以与 Hardhat 一起使用它。
注意,我们需要为当前工作目录设置一个 repo,所以如果还没有的话,请使用 git init。
然后复制并粘贴以下内容到版本库根目录下的一个新的foundry.toml文件中:

[default]
src = 'contracts'
test = 'test'
out = 'artifacts/contracts'
libs = ['lib']

这将设置目录结构以便与 Hardhat 保持一致。Foundry 测试可以使用正常的 MyContract.t.sol 命名放在标准测试文件夹中。
从这里我们可以在Hardhat 中使用Foundry进行测试和部署。就我个人而言,我喜欢 Hardhat 的脚本环境(特别是对于复杂的部署),但也认识到使用 Foundry 进行测试和模糊处理的好处。在同一个代码库中使用这两个应用程序,可以提供两个最佳选择。

使用 Solidity 测试

在我们开始编写单元测试之前,需要安装标准库

forge install foundry-rs/forge-std

然后我们可以将其导入测试文件中,该文件的名称将与我们的合约相同,后缀是.t.sol。

pragma solidity ^0.8.13;

import "forge-std/Test.sol";

contract MyContractTest is Test {

  testWhatever(uint256 var1) public {
    uint256 var2 = 1;
    assertEq(var1,var2);
  }

}

请注意,在上面的例子中,我们给函数名加上了一个前缀test -> testWhatever(),此时出现任何revert都将失败。我们也可以把testFail -> testFailWhatever()作为前缀,这样就需要revert才能通过。

另外请注意,在上面的例子中,我们在var1中传入了一个uint256变量。这个值将被fuzzed(模糊),这意味着它将用各种不常用的值在函数上循环,试图创造一个边缘情况,导致回退(revert)。在这种情况下,任何不是1的值都会由于assertEq()函数而导致测试失败。
也可以创建自定义断言,例如:

function myAssertion(uint a, uint b) {
  if (a != b) {
    emit log_string("a != b");
    fail();
  }
}

然后,我们可以在整个合约中使用这个断言,或者建立一个自定义断言库,并类似于我们先前导入标准库的方式来导入它们。

如果代码库包含许多不同的智能合约,可以使用 --match-contract将单个合约和它的依赖关系分离出来,甚至可以使用--match-test命令行选项进行特定测试。

forge test --match-test optionalSpecificTest --match-contract optionalSpecificContract

另外,我们可以使用forge run来执行一个单一的solidity "脚本 "。

forge run src/Contract.sol              //  执行单个脚本
forge run src/Contract.sol --debug // open script in debugger
forge run src/Contract.sol --sig "foo(string)" "hi" //  执行单一函数

一旦我们发现了一个bug,我们可以使用-v命令来提高提示程度并获得更多细节。

debug with logs -vv
debug with traces for failing tests -vvv
debug with traces for all tests -vvvv

追踪(Traces)是一种非常强大的方式,可以仔细观察智能合约执行过程中发生的事情。这有点像在命令行上有Tenderly.co

还有一个交互式调试器(debugger),这让我想起了1990年的Linux调试器。我没有经常使用,但这里有更多的说明: https://book.getfoundry.sh/forge/debugger.html

forge test --debug "testSomething"

可以在本地分叉(fork)一个区块链,然后关联外部智能合约一起测试我们的合约。如果你想与其他defi协议交互,例如Uniswap,或者使用真实的市场数据执行压力测试,这很有用。

forge test --fork-url https://eth-mainnet.alchemyapi.io/v2/abc123alchmeyApiKey

Gas优化

编译时的合约Gas报告可以通过foundry.toml配置来设置

gas_reports = ["MyContract", "MyContractFactory"]

然后用forge test -gas-report选项执行命令。

优化函数的一个方法是使用测试合约,并在修改前后进行快照对比:

forge snapshot --snap gas1.txt
// make some changes
forge snapshot --diff gas1.txt

这将提供之前的Gas报告和当前快照之间的差异。

用Slither进行安全分析

当涉及到智能合约安全时,Slither绝不是一个简单的解决方案,但它是有用的,并提供了一些自动检查,如检查重入错误。为了使用slither,我一般会切换到WSL(linux的Windows子系统),它可以用以下命令安装(注意0.8.13是目前foundry演示合约中使用的solc版本。你可以把它修改为Solidity文件中设置的任何版本)。

apt install python3-pip
pip3 install slither-analyzer
pip3 install solc-select
solc-select install 0.8.13
solc-select use 0.8.13

然后把以下复制到我们工作的主目录下一个叫slither.config.json的文件里

{
  "filter_paths": "lib",
  "solc_remaps": [
    "ds-test/=lib/ds-test/src/",
    "forge-std/=lib/forge-std/src/"
  ]
}

然后运行一个测试,使用

slither src/Contract.sol

在官方readme中有更多关于slither的信息:https://github.com/crytic/slither

Foundry作弊代码

Foundry有一套作弊代码,它可以对区块链的状态进行修改,以方便在测试时使用。这些代码可以直接执行合约:0x7109709ECfa91a80626fF3989D68f67F5b1DD12D 进行调用,但更多时候是通过标准库和vm对象执行。
重要的作弊代码有:

  • vm.warp(uint256) external; 设置 block.timestamp
  • vm.roll(uint256) external; 设置 block.height.
  • vm.prank(address) external; 设置地址作为下一次调用的msg.sender
  • vm.startPrank(address) external; 设置地址作为所有后续调用的msg.sender
  • vm.stopPrank() external; 重置后续调用msg.sender为address(this)。
  • vm.deal(address, uint256) external; 设置一个地址的余额,参数:(who,newBalance)。
  • vm.expectRevert(bytes calldata) external; 期待下次调用时出现错误。
  • vm.record() external; 记录所有存储的读和写。
  • vm.expectEmit(true, false, false, false); emit Transfer(address(this)); transfer(); 检查事件主题1在两个事件中是否相等
  • vm.load(address,bytes32)外部返回(bytes32); 从一个地址加载一个存储槽
  • vm.store(address,bytes32,bytes32) external; 将一个值存储到一个地址的存储槽中,参数(who, slot, value)。

这些可以用来改变测试的过程,如在这个例子中,告诉测试套件在调用时期望一个标准的算术错误。

vm.expectRevert(stdError.arithmeticError);

完整的列表在这里:https://github.com/foundry-rs/forge-std/blob/master/src/Vm.sol

部署和使用合约

Foundry也可以用来部署并与智能合约交互。

要部署一个合约,我们可以使用下面的命令:

forge create --rpc-url https://mainnet.infura.io --private-key abc123456789 src/MyContract.sol:MyContract --constructor-args "Hello Foundry" "Arg2"

注意我们在生产中部署时不应该使用硬编码的私钥。一个选择是使用-ledger或-trezor来通过硬件钱包执行。另外,也可以使用环境变量来存储私钥。

另一种环境变量来存储私钥:
Linux/Mac:
--privateKey $privateKey

export privateKey=abc123

Windows Powershell:
--privateKey $env:privateKey

$env:privateKey = "0x123abc"

私钥不应该包含0x前缀,否则你会得到一个错误 "Invalid character ‘x’ at position 1(无效字符'x'在位置1)"

我们还可以使用forge命令在etherscan上验证合约,以便我们能够使用Etherscan的UI和Metamask与之交互。

forge verify-contract --chain-id 1 --num-of-optimizations 200 --constructor-args (cast abi-encode "constructor(string)" "Hello Foundry" --compiler-version v0.8.10+commit.fc410830 0xContractAddressHere src/MyContract.sol:MyContract ABCetherscanApiKey123

也可以使用forge来扁平化合约,其中包括外部合约的依赖关系合并到一个文件中:

forge flatten --output src/MyContract.flattened.sol src/MyContract.sol

要生成ABI,可以使用以下命令:

forge inspect src/MyContract.sol abi

注意任何ABI都可以转换为接口并直接在solidity中使用: https://gnidan.github.io/abi-to-sol/

如果合约已经被验证,我们也可以使用以下命令来生成一个接口。

cast interface 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984

用 Cast 交互

我们可以call 方式调用合约请求链上数据。我们也可以提供凭证(私钥)来发送一个交易,就像我们在metamask中签署一个交易一样。

cast call 0xabc123 "totalSupply()(uint256)" --rpc-url https://eth-mainnet.alchemyapi.io

cast send 0xabc123 "mint(uint256)" 3 --rpc-url https://eth-mainnet.alchemyapi.io --private-key=abc123

一旦区块被确认,也可以获取交易本身的信息:

cast tx 0xa1588a7c58a0ac632a9c7389b205f3999b7caee67ecb918d07b80f859aa605fd

也可以通过flashbots保护执行交易,cast使用--flashbots 参数发送交易。

最后,你可能想估算一个Gas成本,这也可以用cast来完成:

cast estimate 0xabc123 "mint(uint256)" 3 --rpc-url https://eth-mainnet.alchemyapi.io --private-key=abc123

Foundry为测试和审计智能合约提供了一个快速、高效的框架。我希望这个教程是有用的,如果想进一步深入了解,别忘了查看官方文档。https://book.getfoundry.sh

Foundry中文文档: https://learnblockchain.cn/docs/foundry/i18n/zh/
转载:https://learnblockchain.cn/article/4524

tx.origin、msg.sender有什么不一样

刚看了篇文章,感觉还可以,分享一下,原文在此:
tx.origin vs msg.sender. I have been recently playing ethernaut… | by David Kathoh | Medium

吃过亏的人肯定知道。

msg.sender: 指直接调用智能合约功能的帐户或智能合约的地址
tx.origin: 指调用智能合约功能的账户地址,只有账户地址可以是tx.origin

再来一张图片:

目前不少安全问题都是因为这两个概念理解差异造成的,希望你在写合约的时候用你需要的东西。

转载自:https://learnblockchain.cn/article/3568

how to get eth_signTypedDataV4 signature in web3.py

EIP712 signatures can generated using the https://eth-account.readthedocs.io/en/stable/eth_account.html#eth_account.messages.encode_structured_data function to encode the EIP712 message, and use web3 to sign it.

msg = { "types": 
{ "EIP712Domain": [], "Type":  []},
  "domain":domain,
  "primaryType": 'Type',
  "message":message }

encoded_data=encode_structured_data(msg)
web3.eth.account.sign_message(encoded_data,privateKey)

https://ethereum.stackexchange.com/questions/114190/how-to-get-eth-signtypeddatav4-signature-in-web3-py