您正在查看: Surou 发布的文章

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

Web3学习笔记:交易所钱包管理系统

交易所作为web3世界的核心枢纽,托管着大量加密资产,其钱包管理关系到大量用户的资产安全,因此很有必要对其钱包管理系统研究学习一番。这里我整理了一篇个人从产品设计者视角的学习笔记,希望能够帮助到大家学习。

交易所钱包系统组成

交易所钱包系统设计主要需要考虑到安全性便捷性。为了兼顾两者,交易所大部分资产用冷钱包管理,以保证资产安全;而少部分资产使用热钱包管理,以方便用户提现资产。

热钱包系统

热钱包系统由用户充值钱包、归集钱包、提现钱包、手续费钱包等组成

  • 用户充值钱包:用户需要向交易所进行代币充值 ,需要为每一名用户分配一个地址,用于充值代币,其私钥保存在服务端以供后续资金归集时签名。
  • 归集钱包:资产分散在各用户充值钱包中不方便管理,因此当充值钱包达到一定规模时需要进行资金归集,将资金统一归集至归集钱包。
    大量资金统一管理在一个钱包中是存在比较大的安全风险,所以需要对归集钱包中的资产进行分配并转移。
  • 提现钱包:为了方便用户提现和资金安全,一般会将20%资金转移到一个提现钱包(也可能是几个钱包),这个钱包专门用于客户提币使用。
  • 手续费钱包:归集资金、转移资金、用户提现这几个操作均需要交易费,当其他钱包ETH不足抵扣交易费时,由该钱包转入一定的ETH作为交易费。

冷钱包系统

冷钱包系统一般包含系统冷钱包和BOSS钱包组成。

  • 系统冷钱包:一般会将20%~30%的资金会转移至系统冷钱包,当提现钱包资金不够时才会在系统冷钱包进行划转。
  • BOSS钱包:一般50%以上资金会转移至boss钱包,由公司一个或多个老板控制。

钱包系统业务流程

交易所钱包系统主要涉及业务流程:

  • 注册— 生成充值地址
  • 用户充值处理
  • 资金归集
  • 资金分配转移
  • 提现转账

注册—— 生产充值地址

当用户注册账户,需要对应创建一个钱包,为用户分配一个单独的充值地址,私钥由保存在服务端以用于资金归集。

用户充值处理

我们需要持续监听充值钱包代币入账及区块确认数,一般12个区块确认后才认定为入账成功。入账成功后,需要判断平台是否支持该代币,若支持则增加账户对应代币的余额,至此完成用户充值处理。

资金归集

用户充值完成后,我们需要判断用户充值钱包该代币资产价值,一般当其价值达到1000U,我们需要将钱包中资金归集到归集钱包。

归集前需要判断预估gas是否过高(超过100 gas),若过高则等待降下来,再进行归集。
另外,需要判断钱包是否有足够ETH以抵扣交易费,若不足则需从手续费钱包转入0.01ETH再进行归集。

资金分配转移

归集钱包为热钱包,当大量资产都在一个热钱包是存在极大风险的,因此我们将资产转移。

筛选统计:在进行划转和分配之前,我们需要对归集钱包中资产进行筛选统计,筛选出平台支持且有余额的代币,然后统计筛出代币的余额。

资金分配:我们将80%的资产转移至冷钱包地址,20%继续转移至提现热钱包中。这样设计兼顾安全性和便捷性,冷钱包控制相对来说保证了安全性;而大部分玩家都是把大部分资金存放在交易所中,热钱包20%流动资金是足够支撑用户的提币需求,因此也一定保证了便捷性。

资金划转:我们一般为固定周期进行分配转移,一般设计为一周划转一次,具体周期和时间可以根据业务和公司情况而定。我们根据代币类型采用不同划转方式,ETH直接根据分配资金转账,ERC-20代币使用合约批量打包转账(节省交易费)。

提现转账

用户发起提现后,由于Nonce机制存在,并不直接将交易发送至网络,而使用本地队列排队,接收到上一条成功确认记录后,再获取最新Nonce,进行下一条转账发送。

发送前,判断余额是否充足,若不足则需申请从系统钱包手动转入足够的代币后再进行转账。

私钥管理方案

多签

提示:以太坊本身不支持多签,需要使用智能合约实现,合约本身又存在安全风险,因此交易所的以太坊私钥一般采用该方案保存

普通用户可以将私钥/助记词保存在软件、纸上或者通过大脑记忆保存。但对于拥有巨额资产的交易所而言,普通用户的私钥管理方式根本无法适用,否则将存在单点保存的风险。

为了避免一个私钥的丢失导致地址的资金丢失,达到风险分散的目的,一般会采用多重签名技术。所谓多重签名技术,就是把一个钱包地址的控制权交给多个密钥,这些密钥保存在不同的地点,并分别生成签名。举个简单的例子:

2-3多重签名,表示3个人拥有签名权,而两个人签名就可以支配这个账户里的资金;
1-2多重签名,表示2个人可以签名,两个人拥有私钥,谁都可以来支配这笔资金。

在热钱包方面,一般采用2-3多重签名,需要三个私钥持有人中的两个分别进行授权,才能完成签名。
在冷钱包方面,一般采用的是2-2多重签名,即每个私钥的使用需要两个人双重授权才能进行提币,以保证冷钱包安全。

备份

在采用多重签名的同时,为了进一步降低私钥丢失或损坏的风险,还对私钥进行了备份处理:
对于热钱包,备份私钥存储办公室附近的银行保险柜。
对于冷钱包,一般备份两份,一份存在办公室附近的银行保险柜,一份存在异地某一家银行的保险柜;同时,异地银行保险柜必须由两个不同的人掌握,掌握银行保险柜的两个人不得乘坐同一辆交通工具。

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