您正在查看: Ethereum-优秀转载 分类下的文章

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

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

EIP 1014: CREATE2 指令

EIP 1014: CREATE2 指令

作者 类型 分类 状态 创建时间
Vitalik Buterin Standards Track Core Final 2018-04-20

#规范

添加了一个新的0xf5 操作码指令CREATE2 ,它使用4个栈参数:endowment, memory_start, memory_length, salt 。 0xf5 指令的行为和CREATE相同,只不过它使用keccak256( 0xff ++ address ++ salt ++ keccak256(init_code))[12:] 而不是 发送者+nonce hash来计算出合约地址。

CREATE2CREATEgas 模式一样,但是有一个额外的 GSHA3WORD * ceil(len(init_code) / 32) hash 计算消耗(hashcost),在计算出地址和执行 init_code代码之前,hashcost 与内存扩展 及 CreateGas 一起被扣除。

  • 0xff 为一个字节
  • address20 字节
  • salt32 直接 (占用一个栈).

因此,最后一次哈希keccak256( 0xff ++ address ++ salt ++ keccak256(init_code))[12:] 的内容始终正好是85字节长, 在2018-08-10进行的核心开发者会议上决定使用上述公式。

#动机

允许使用链上尚不存在但可以依赖的地址进行交互,对于像 counterfactually 类似的状态通道中非常有用。地址由特定初始化代码及可控的salt控制。

#原理阐述

#Address 计算公式

  • 确保使用此方案创建的地址不会与使用传统的 keccak256(rlp([sender, nonce])) 公式创建的地址冲突,而0xff 只可能是很长字节的 RLP编码前缀。
  • 确保hash 的转载内容是 固定字节大小

#Gas 消耗

由于地址计算依赖于对 init_code 的hash ,因此如果反复对大块 init_code 执行 hash,可能使网络客户端容易受到DoS攻击,因为内存扩展仅支付一次。 该EIP使用与 SHA3 操作码相同的每字成本。

#澄清

init_code 是执行后会生成运行时字节码(存入链状态)的代码,通常由高级语言用来实现构造函数。合约

此EIP可能产生冲突。 冲突行为EIP 684有指出:

如果使用创建交易或CREATE(或将来的CREATE2)操作码去尝试创建合约,并且目标地址已经具有非零 nonce 或非空代码,则该创建将立即抛出,如果初始化代码中的第一个字节是无效的操作码,则同样会抛出此错误。 适用与从创世纪块开始追溯。

具体来说,如果 noncecode 非零,则创建操作将失败。

以及 EIP 161

在执行初始化代码之前,帐户创建交易和CREATE操作应随机数增加1。

这意味着,如果在交易中创建合约,则 nonce 立即为非零,其副作用是,同一交易中的冲突将始终失败-即使是通过 init_code 本身执行 。

还应注意,SELFDESTRUCTnoncecode 没有立即生效,因此不能在一次交易中销毁和重新创建合约。

#示例

示例 0

  • address 0x0000000000000000000000000000000000000000
  • salt 0x0000000000000000000000000000000000000000000000000000000000000000
  • init_code 0x00
  • gas (assuming no mem expansion): 32006
  • result: 0x4D1A2e2bB4F88F0250f26Ffff098B0b30B26BF38

示例 1

  • address 0xdeadbeef00000000000000000000000000000000
  • salt 0x0000000000000000000000000000000000000000000000000000000000000000
  • init_code 0x00
  • gas (assuming no mem expansion): 32006
  • result: 0xB928f69Bb1D91Cd65274e3c79d8986362984fDA3

示例 2

  • address 0xdeadbeef00000000000000000000000000000000
  • salt 0x000000000000000000000000feed000000000000000000000000000000000000
  • init_code 0x00
  • gas (assuming no mem expansion): 32006
  • result: 0xD04116cDd17beBE565EB2422F2497E06cC1C9833

示例 3

  • address 0x0000000000000000000000000000000000000000
  • salt 0x0000000000000000000000000000000000000000000000000000000000000000
  • init_code 0xdeadbeef
  • gas (assuming no mem expansion): 32006
  • result: 0x70f2b2914A2a4b783FaEFb75f459A580616Fcb5e

示例 4

  • address 0x00000000000000000000000000000000deadbeef
  • salt 0x00000000000000000000000000000000000000000000000000000000cafebabe
  • init_code 0xdeadbeef
  • gas (assuming no mem expansion): 32006
  • result: 0x60f3f640a8508fC6a86d45DF051962668E1e8AC7

示例 5

  • address 0x00000000000000000000000000000000deadbeef
  • salt 0x00000000000000000000000000000000000000000000000000000000cafebabe
  • init_code 0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef
  • gas (assuming no mem expansion): 32012
  • result: 0x1d8bfDC5D46DC4f61D6b6115972536eBE6A8854C

示例 6

  • address 0x0000000000000000000000000000000000000000
  • salt 0x0000000000000000000000000000000000000000000000000000000000000000
  • init_code 0x
  • gas (assuming no mem expansion): 32000
  • result: 0xE33C0C7F7df4809055C3ebA6c09CFe4BaF1BD9e0

本翻译采用BY-NC-ND许可协议,译者:深入浅出区块链 Tiny熊。

转载自:https://learnblockchain.cn/docs/eips/eip-1014.html

解构 Solidity 合约 #4: 函数体

这是解构系列另一篇。如果你没有读过前面的文章,请先看一下。我们正在解构一个简单的Solidity合约EVM字节码

我们已经走过了很长的路,不是吗?首先,我们理解了合约的创建时间和运行时字节码之间的区别;接下来,我们理解了来自任何调用或交易的执行入口是如何通过函数选择器被路由到特定的函数的;最后,我们看到了传入的交易数据是如何被解包给函数使用的,以及函数产生的数据是如何通过函数包装器为用户重新打包的。在这一节中,我们将(最后)看看函数的实际执行情况,或者我们通常称为 "函数体" 的部分。

函数体正是函数包装器在解开传入的calldata后所跳入的部分。当一个函数体被执行时,函数的参数应该安然无恙的在堆栈中(如果数据是动态的,则在内存中),等待被使用。让我们看看balanceOf(address)函数的实际应用。这个函数应该接收一个 address,并返回这个地址相应的 uint256 的余额。

让我们回到Remix,像以前一样编译和部署合约,然后调用balanceOf函数,把部署合约时用的地址作为参数。这应该返回数字10000,因为它是最初赋值给构造函数代码中部署合约的地址的,我们在部署合约时使用了这个地址。

好了,现在让我们来调试一下这个交易。

你会注意到的第一件事是,调试器把我们放在了指令252处。如果你看一下解构图,在包装器的蓝色部分,你应该看到balanceOf函数包装器将指令175处重定向到251的JUMPDEST指令。正如我们之前多次看到的,Remix将我们精确地放在了函数主体即将被执行的位置。

图1. 函数包装器将执行重定向到函数体(指令175的蓝色虚线)


图2. 函数体的执行,来自于函数包装器(指令251处的蓝色虚线)。

现在,如果你看一下堆栈,你会发现它最上面的值是我们调用balanceOf的地址。包装器已经完成了正确解包calldata的工作。所以我们准备通过指令251到290,即balanceOf函数体。

指令252推送了一个20字节的0xffffffffffff值,并使用AND操作码将32字节的地址 mask(掩码)为正确的类型(记住,以太坊的地址是20字节的,而堆栈的操作是32字节的字)。

在指令274至278中:

字节码将把地址从堆栈上传到内存。它需要这个地址用于即将到来的 SHA3 操作码。如果你看一下黄皮书,SHA3操作码有两个参数:计算哈希值的内存位置和哈希值的字节数。

但是,为什么代码会使用SHA3操作码?这个函数想从balances映射中读取。更确切地说,它想读取映射到的地址的值。如果你了解映射在存储中的布局,变量槽 (在这里是1)的哈希值,因为balances被定义为第二个变量(totalSupply_是第一个变量,在槽0),实际的键本身是地址,SHA3需要这两个值寻找的值在存储中的位置。

其槽位置计算方式大概可以表述为:

keccak(
  leftPadTo32Bytes(addr) ++ leftPadTo32Bytes(1)
) 
用 Solidity 代码表示:
bytes32 aliceBalanceSlot = keccak256(
    abi.encodePacked(uint256(uint160(address)), uint256(1))
);

所以,我们已经得到了内存中的地址,但现在我们需要内存中的插槽。这就是指令279和283之间接下来发生的事情:

数字0x01被存储在内存位置0x20。现在内存保存着第一个字的地址,即内存位置0x00,和第二个字的槽,即内存位置0x20。耶! 我们准备调用SHA3。

于是在指令284和287之间调用了它。

当287号指令调用SHA3时,堆栈包含0x00(SHA3的起始位置)和0x40(SHA3的长度),这基本上是告诉EVM在前两个32字节的字中对内存中的任何内容进行哈希。32个字节的十六进制是0x20,所以0x20+0x20等于0x40。

现在,SHA3在堆栈中留下了32字节的哈希值,这是一个非常长的十六进制数字,比以太坊地址长很多。这个哈希值是合约存储中的位置,传递给balanceOf的地址的余额就存储在这里。你可以使用Remix调试器中的Storage completely loaded面板来直观地看到这一点。你应该在第二个存储对象中找到一个匹配的位置。

在这个位置上存储了什么?数字10000,或者十六进制的0x2710。在第288条指令中,SLOAD接收了从存储位置(我们的哈希值)读取的参数,并将0x2710推到堆栈。

最后,在第289条指令中,SWAP1重新显示了函数包装器的JUMPDEST位置(0x70,或112),第290条指令中的JUMP将我们带回函数包装器的输出部分,它将重新包装0x2710以返回给用户。

我强烈建议你回顾一下我们刚才对balanceOf的调试过程,再对totalSupply和transfer函数的进行调试。前者非常简单,而后者要复杂得多,但基本上是由相同的结构块组成的。秘密在于理解如何从映射中读取数值和写入映射。真的没有什么更多的东西了。

现在让我们回到大解构图:

图3. 函数包装器之后的函数体。

正如我们之前所讨论的,函数体都集中在函数封装器之后。执行流从包装器中跳到它们,并在执行完每个函数的指令后返回到包装器。
如果你仔细看这张图,在函数体之后有一大块代码,叫做 "元数据哈希"。这是一个非常简单的结构,在下一篇文章我们将解析一下这个部分。

转载:https://learnblockchain.cn/article/5263