区块链中文技术社区 - 2023年2月
type: channel
link: https://www.bcskill.com/index.php/2023/02/
description: 区块链,EOS,Ethereum,IPFS,Dapp技能学习
items: 理解账户抽象 #3 - 钱包创建
items: 理解账户抽象 #2：使用Paymaster赞助交易
items: 理解账户抽象 - #1
items: 以太坊迈向full sharding的第一步，EIP-4844全面解析
items: 合约安全 - 选择器碰撞
items: 接收ETH receive和fallback
items: Solidity ABI编码解码
items: 实例验证 - call delegatecall
items: 实例验证 - 访问私有数据
items: 智能合约安全审计入门篇 —— 访问私有数据

理解账户抽象 #3 - 钱包创建
type: item
date: 2023-02-26T17:27:00+08:00
description: 前面 2 篇我们介绍了账户抽象，第一篇：如何使用智能合约钱包来实现 EOA 钱包一样的功能，第二篇 : 介绍如何使用第三方来代为支付手续费。 钱包创建 我们还没有解决的问题是每个用户的钱包合约首先是如何在区块链上如何生成的。部署合约的 "传统" 方式是使用EOA来发送一个没有接收者（没有to字段）的交易，其中包含合约的部署代码。这在将是相当不满意的，因为我们刚刚做了很多工作，使某人可以在没有EOA的情况下与链交互。如果用户需要自己的EOA才能开始，那这一切是为了什么？ 为了明确我们的想法，一个想要钱包但还没有钱包的人应该能够在链上拥有一个全新的钱包，要么用ETH支付他们自己的Gas（即使他们还没有钱包），要么通过找到一个愿意为他们的交易付Gas的支付者（我们在第二部分中涉及到），需要应该能够做到这一点，而不需要创建一个EOA。 还有一个不太明显的目标也相当重要。 当我创建一个新的EOA时，我可以在本地生成我的私钥，并在不发送任何交易的情况下申请我的账户。 我可以告诉别人我的地址，并在我自己发送交易之前开始接收ETH或代币。 我们希望我们的钱包有同样的属性，也就是说，我们应该能够在实际部署我们的钱包合约之前告诉别人我们的地址并接收资产。 前提：用CREATE2确定的合约地址 关于在我们实际部署合约之前就能在我们的地址接收资产， 这一点是对我们需要实现他的一点提示。这意味着，尽管我们可能还没有部署钱包合约，但我们需要知道当我们最终能够真正部署它时，它将在什么地址上。 一个合约最终将被部署但尚未部署的地址被称为反事实地址（counterfactual address）。 实现这一目标的关键因素是CREATE2操作码，它在一个地址上部署一个合约，这个地址可以通过以下输入确定地计算出来。 调用CREATE2的合约的地址 一个盐（salt），它可以是任何32字节的值 被部署的合约的init代码 初始代码是一些EVM字节码，指定了一个函数，该函数在执行时返回一个不同的EVM字节码，保存作为新部署的智能合约。这是一个有趣的花絮：很多人都没有意识到。当你部署一个合约时，你提交的代码并不是最终出现在链上合约中的代码。特别是，多次使用相同的初始代码并不能保证部署的合约会有相同的代码，因为初始代码可能会从存储中读取或使用TIMESTAMP等操作码。 译者注：合约的字节码分为创建字节码及运行时字节码， 可参考 理解 EVM 专栏 的 解构Solidity合约 #1 - 字节码 第一次尝试： 入口点部署任意的合约 既然我们知道了CREATE2，我们的第一个计划就很简单。我们将让用户传入初始代码，如果合约还不存在，则由入口点部署合约。首先，我们将为用户操作添加一个新的字段： struct UserOperation { // ... bytes initCode; } 然后，我们将更新入口点handleOps的验证部分，做以下工作： 作为验证用户操作的一部分，如果该操作有非空的initCode，则使用CREATE2来部署一个带有该initCode的合约。 然后继续进行其他正常的验证工作。 调用新创建的钱包的validateOp方法 然后，如果该操作有一个paymaster，调用paymaster的validatePaymasterOp方法 这是一个很好的尝试! 它实现了上面讨论的所有目标：用户可以部署任意的合约，并提前知道它们最终的地址，而且部署可以由paymasters或用户自己赞助（如果他们将ETH存入合约最终的地址）。 但是有一些缺陷，这些缺陷都是围绕着这样一个事实：我们要求用户提交及入口点要验证一个任意的字节码。 当一个paymaster 看到一个用户操作时，它不能合理地分析这一串字节码来决定它是否要为其付款。 当用户提交一串字节码来部署合约时，他们不能轻易地验证他们提交的字节码是否符合他们的要求。如果用户使用一个工具来部署他们的合约，那么如果该工具是恶意的或被黑客攻击的，它可以提交initCode，将后门安装到部署的合约中，而这种攻击不容易被发现。 回顾第一篇，捆绑者希望对它包含在捆绑中的每个操作进行模拟验证，这样它就不会最终包括那些未能通过验证的操作，然后它就不得不自掏腰包支付Gas。但由于initCode是任意代码，它很容易在模拟过程中成功，但在执行过程中失败。 我们需要一种方法让用户在不提交任意字节码的情况下部署合约，并让其他参与者能够对部署行为有一些保证。 像往常一样，当我们想要更多的执行保证时，就是引入新的智能合约的时候了。 更好的尝试： 引入工厂 与其让入口点接受任意字节码并调用CREATE2，我们将允许用户选择一个合约作为调用CREATE2的合约。这些选择的合约，我们称之为工厂，如果他们愿意，可以专门创建不同种类的钱包合约。 例如，…

理解账户抽象 #2：使用Paymaster赞助交易
type: item
date: 2023-02-26T16:54:00+08:00
description: 理解账户抽象第二篇，看看如何使用第三方代替自己支付。 在本系列的第一篇中，我们从头开始创建了就有EOA的功能的智能合约钱包，并通过允许用户选择自己的自定义验证逻辑进行改进。但是现在，钱包仍然需要支付Gas，这意味着钱包所有者需要找到一种方法来获得一些ETH，然后才能在链上执行任何操作。 如果我们想让钱包主人以外的其他人代替我们支付Gas呢？ 有一些很好的理由支持我们加入这个功能： 如果钱包所有者是一个区块链新手，那么在执行链上操作之前需要获得ETH是一个巨大的绊脚石。 一个dapp可能愿意为其方法支付Gas费，这样Gas费就不会吓跑潜在用户了 一些赞助者（或项目方）可能会允许钱包以ETH以外的其他代币支付Gas费用，例如以USDC支付。 为了保护隐私，用户可能想从混合器中提取资产到一个新的地址，并将Gas费用记入一个与他们无关的账户中 引入Paymaster 假设我是一个想为其他人支付Gas费的dapp。我可能不想在任何地方为每个人的Gas付费，所以我需要把自定义逻辑放到链上，它可以查看用户的操作并决定是否要为该操作付费。 把自定义逻辑放到链上的方法是部署一个合约，我们称之为paymaster（支付者或付款人）。 它将有一个方法来查看用户操作并决定是否愿意为该操作付费： contract Paymaster { function validatePaymasterOp(UserOperation op); } 然后，当一个钱包提交一个操作时，他们需要指出他们希望哪个支付方（如果有的话）来支付他们的Gas。 我们将在 "用户操作"中添加一个新的字段来指定支付者。 我们还将在用户操作中添加一个字段，钱包可以用它来向支付者传递任意的数据，以帮助它说服支付者支付其费用。 例如，这可能是由支付者的所有者在链外签署的内容。 struct UserOperation { // ... address paymaster; bytes paymasterData; } 接下来，我们将改变入口点的handleOps，以利用新的paymaster。 现在它的行为将是： 对于每个操作（op）: 在操作者指定的钱包上调用validateOp。 如果该操作有一个paymaster地址，那么对该paymaster调用validatePaymasterOp。 任何验证失败的操作都会被丢弃。 对于每个操作，在操作的发送者钱包上调用executeOp，跟踪我们使用了多少Gas，然后将ETH转账到执行者那里以支付这些Gas。如果该操作有一个paymaster字段，那么这个ETH来自paymaster。否则，它就像以前一样来自钱包。 就像钱包一样，paymaster通过入口点（entrypoint）的存款方法存入他们的ETH，然后才能用于支付操作。 执行者同时调用支付者合约和用户的智能合约钱包，以确定用户的交易是否可以被赞助。 这其实是很简单的，对吗？ 我们只要让捆绑器更新它的模拟操作和... 在上一篇文章中，钱包向捆绑者退款，捆绑者会先模拟操作，试图避免执行验证失败的操作，因为这意味着钱包不会付款，所以捆绑者将承担Gas成本。 这里也出现了同样的问题： 捆绑者想避免提交验证失败的操作，因为paymaster不会付款，而捆绑者又要负担成本。 起初，我们似乎可以对validatePaymasterOp 施加和validateOp同样的限制（即它只能访问钱包和它自己的相关存储，不能使用被禁止的操作代码），然后捆绑者可以在模拟钱包的validateOp的同时，简单地为用户操作模拟validatePaymasterOp。 但是，这里有一个问题 因为存储限制说一个钱包的validateOp只能访问该钱包的相关存储，我们知道一个捆绑程序中的多个操作的验证不能相互干扰，只要它们来自不同的钱包，因为它们访问的共同存储非常少。 但是一个paymaster的存储在捆绑中所有使用该paymaster的操作中是共享的。 这意味着一个validatePaymasterOp的行为有可能导致使用同一个paymaster的交易包中的许多其他操作的验证失败。 恶意的paymaster可以利用这一点来破坏系统。 为了防止这种情况，我们引入了一个信誉系统。 我们将让捆绑者跟踪每个paymaster最近验证失败的频率，并通过节流或禁止使用该paymaster的操作来惩罚那些经常失败的paymaster。 如果一个恶意的paymaster可以创建许多自己的实例（ Sybil 攻击），这个信誉系统就不会起作用，所以我们要求paymaster用ETH做抵押。这样一来，它就不能从拥有多个账户中获益。 让我们在EntryPoint添加新的方法来处理抵押： contract EntryPoint { // ... …

理解账户抽象 - #1
type: item
date: 2023-02-26T13:17:00+08:00
description: 本文提供一个新的视角来理解账户抽象（Account Abstraction）。通过从零开始设计智能合约钱包，你会明白几个简单选择之后，让 EIP-4337 如此复杂的原因。 理解账户抽象 - #1 通过从零开始设计智能合约钱包，你会明白几个简单选择之后，让 ERC-4337 如此复杂的原因。 账户抽象(Account Abstraction)即将完全改变我们与区块链的交互方式。但EIP-4337提出的账户抽象的版本艰难阅读，要理解为什么有这么多的参与者，为什么他们会以这样的方式交互，是一件很困难的事情。 会不会有更简单的东西呢？ 在这篇文章中，我将介绍试图设计一个简单的账户抽象的过程，将看到随着我们增加更多的需求和解决出现的问题，我们最终会得到一个复杂度膨胀的东西，并且越来越接近于EIP-4337。 这篇文章的目标受众是对智能合约有一定了解，但对账户抽象没有特别了解的人。 因为本文是在探索发明账户抽象的过程，所以会有很多时候，我描述的API或行为与EIP-4337的最终版本不一致。 例如，当我列出一个用户操作的字段时，请不要认为这些是实际的字段。这些字段代表了在最终版本之前对用户操作的第一次尝试性定义。 好了，准备好了吗？我们开始吧。 目标：创建一个能保护我们资产的钱包 为了开始工作，让我们发明一种方法来保护我们最宝贵的资产。我们希望能够用一个单一的私钥来签署大多数交易（就像一个典型的账户），但我们无价的Carbonated Courage NFT 应该只有在我们用第二把私钥签署时才有可能转移，我们会把它锁在由”三头犬“看守的银行保险库里。 这里是第一个问题： 每个以太坊账户要么是智能合约，要么是外部拥有的账户（EOA），其中后者是由链外使用私钥控制的。持有这些资产的账户应该是智能合约还是EOA？ 事实上，资产持有者必须是一个智能合约。如果它是一个EOA，那么资产总是可以通过EOA的私钥签署的交易来转移，这就绕过了我们想要的安全性。 因此，与今天的大多数人不同，我们在链上的存在将由一个智能合约来代表，而不是一个EOA，我们将其称为智能合约钱包，或只是一个 "钱包"。 我们需要一种方法来向这个智能合约发布命令，以便它执行我们想要的行动。特别是，我们（从EOA 账户发起）需要能够命令智能合约进行任何形式的转账或调用。 每个希望自己的资产以这种方式得到保护的用户都需要自己的智能合约。不可能有一个大的合约持有多个人的资产，因为生态系统的其他应用假定一个地址代表一个实体。 例如，如果有人想向联合钱包合约中的某人发送NFT，NFT的转移API将只允许发送者指定联合钱包的地址，而不是其中的单个用户。 用户操作 我将部署一个钱包智能合约，它将持有我的资产，钱包需要有一个函数，以便我把我希望它进行的调用的信息传递给它。 这里把代表希望我的钱包执行的行动的数据称为用户操作（User Operation）。 因此，钱包的合约看起来像这样： contract Wallet { function executeOp(UserOperation op); } 用户操作的内容是什么？ 首先，需要所有我们通常会传递给eth_sendTransaction的参数： struct UserOperation { address to; bytes data; uint256 value; // Amount of wei sent uint256 gas; // ... } 除此之外，我们还需要提供一些东西来授权请求 -- 也就是说，钱包将查看一块数据（签名）来决定是否要执行该操作。 对于我们的NFT保护的钱包，对于大多数用户的操作，我们会传递一个主密钥签名的操作部分的数据。 但是，如果用户操作是转移我们超级有价值的 Carbonated Courage NFT，那么钱包将需要我们传递由我们两个密钥各自签署的操作的签名。 我们还将需要有一个nonce，以防止重放攻击，即有人可以重新发送以前的用户操作来再次运行它。 struct UserOperation { // ... bytes signature; uint256 nonce; } 这实际上已经达到了目的! 只要我的 Carbonated Courage NFT 被这个合约所持有，没有两个人的签名，它就不能被转让。 虽然钱包可以选择如何解释signature和nonce字段，但我希望几乎所有的钱包都使用signature字段来接收某种覆盖所有其他字段的签名，以防止未经授权的各方伪造或篡改操作。同样地，我希望几乎所有的钱包都能拒绝一个它已经使用过的nonce授权操作。 谁调用智能合约钱包？ 这里有一个未回答的问题是executeOp(op)如何被调用。如果没有我的私钥的签名，它不会做任何事情，我们可以让任何…

以太坊迈向full sharding的第一步，EIP-4844全面解析
type: item
date: 2023-02-26T11:15:56+08:00
description: 简单概括，KZG Ceremony 是实现 EIP-4844 不可绕过的一个初始化环节，而 EIP-4844 是实现以太坊sharding过程中的先行版本。 一、Sharding: 以太坊扩容的长久之计 Sharding 从数据可用性角度扩容，而 rollup 主要从执行层面扩容，一起缓解主网拥堵问题；我认为sharding 可能是突破区块链不可能三角的一次尝试。 下面这张以太坊区块大小的走势图可以从另一个角度说明数据层面扩容的必要性。从创世区块开始，以太坊从底层架构到上层应用都在不断地快速迭代，但平均区块大小仍为 90Kb 左右，最高点也没有质的突破。虽然Rollup从计算层切入，解决以太坊拥堵问题，但性能仍然受制于layer 1的数据存储能力。 由于实现过程的复杂度和安全性考虑，以太坊开发团队将 sharding 分成了多个阶段，其中就包括最近提到的 proto-danksharding 和 danksharding 。整个过程将会是一个历时数年的更新； 在目前的以太坊的数据存储模式下，只有少数特定高配置的机器可以加入网络成为节点，而 full sharding 之后 ethereum 不再需要每个节点都保存全部数据，在降低主网数据存储成本的同时加强了安全性（成为节点的门槛降低，去中心化程度进一步提升，同时降低被攻击的风险）。 二、EIP-4844: 短期高回报，Sharding的精简先行版 EIP-4844 = Proto-Danksharding； Proto来自以太坊研究者的名称 由于实现 danksharding（下一节会分析）的复杂度很高，开发周期至少是以年为单位的。因此 proto-danksharding 是实现 danksharding 前对以太坊的扩容方案，主要实现了 danksharding 中的交易格式、precompile 等设计； 1. Proto-danksharding概述 Proto-Danksharding 主要引入了一种新的 transaction type，也就是 blob-carrying transaction。至此，Rollup 的数据通过blob 的形式以更低的成本向layer 1传输，并作非永久存储。同时，blob 远大于现在的 calldata，可以更好地支持 rollup 上的高TPS。 关于blob： 每个transaction最多挂2个 blob； 每个block理想状态包含8个 blob，约为1MB，最多包含16个 blob，约为2 MB； Blob 不需要像calldata一样作为 history log 被永久存储； 相比 danksharding，节点还是需要对完整的 DA进行验证。 2.Blob-carrying transaction解读 给transaction挂上blob（数据单元） 作用 参考Vitalik在提案中给出的图，Data blob 和当前的 calldata 相似，rollup 可以将交易、证明等数据通过blob上传到 layer 1 来保证 data availability。 成本 Data blob 的设计初衷是支持 rollup 高通量的交易，相比同等大小的 calldata（使用链上存储），blob 的成本将会降低很多（不需要永久存储）。因此，rollup 在维持数据可用性上花费的gas相比之前会显著降低。 容量 每个blob的大小约为125kB（fact: 当前平均block size只有～90kB）。 3. Blob-carrying transaction的价值和挑战 价值 可以把blob看成一种缓存，rollup 提交的交易数据从此以缓存的形式存在。降低对存储硬件的要求，为以太坊提供额外的数据扩容并降低 gas 成本。 挑战：对以太坊节点硬件性能的要求 Ethereum 当前平均区块大小只有～90kB，但是一个 blob 就有～125kB 根据 EIP-4844 的设计，每个slot正常情况下约为1 MB，因此每年增加的数据量为： 1 MB/block 5 block/min 43200 min/month * 12 month/year = 2.47 TB per year 每年增加的数据大小远超过了以太坊数据总量，这样的存储方案显然是不高效的。 解决方案 从短期扩容效果看，由于每个节点仍然需要储存全量历史数据，在实现 EIP-4844 的同时，对一段时间窗以外的 blob（具体的 limit time 还没有最终确定，可能是1个月或是1年）进行自动删除； 从 sharding 的长期利益看，实现EIP-4444，即节点不需要存储全量历史数据，而是只需要参照 history expiry，存储特定时间之后的数据； 这两…

合约安全 - 选择器碰撞
type: item
date: 2023-02-24T22:21:00+08:00
description: 这一讲，我们将介绍选择器碰撞攻击，它是导致跨链桥 Poly Network 被黑的原因之一。在2021年8月，Poly Network在ETH，BSC，和Polygon上的跨链桥合约被盗，损失高达6.11亿美元。这是2021年最大的区块链黑客事件，也是历史被盗金额榜单上第2名，仅次于 Ronin 桥黑客事件。 选择器碰撞 以太坊智能合约中，函数选择器是函数签名 "()" 的哈希值的前4个字节（8位十六进制）。当用户调用合约的函数时，calldata的前4字节就是目标函数的选择器，决定了调用哪个函数。如果你不了解它，可以阅读WTF Solidity极简教程第29讲：函数选择器。 由于函数选择器只有4字节，非常短，很容易被碰撞出来：即我们很容易找到两个不同的函数，但是他们有着相同的函数选择器。比如transferFrom(address,address,uint256)和gasprice_bit_ether(int128)有着相同的选择器：0x23b872dd。当然你也可以写个脚本暴力破解。 大家可以用这两个网站来查同一个选择器对应的不同函数： https://www.4byte.directory/ https://sig.eth.samczsun.com/ 你也可以使用下面的Power Clash工具进行暴力破解： PowerClash: https://github.com/AmazingAng/power-clash 相比之下，钱包的公钥有256字节，被碰撞出来的概率几乎为0，非常安全。 漏洞合约例子 漏洞合约 下面我们来看一下有漏洞的合约例子。SelectorClash合约有1个状态变量 solved，初始化为false，攻击者需要将它改为true。合约主要有2个函数，函数名沿用自 Poly Network 漏洞合约。 putCurEpochConPubKeyBytes() ：攻击者调用这个函数后，就可以将solved改为true，完成攻击。但是这个函数检查msg.sender == address(this)，因此调用者必须为合约本身，我们需要看下其他函数。 executeCrossChainTx() ：通过它可以调用合约内的函数，但是函数参数的类型和目标函数不太一样：目标函数的参数为(bytes)，而这里调用的函数参数为(bytes,bytes,uint64)。 contract SelectorClash { bool public solved; // 攻击是否成功 // 攻击者需要调用这个函数，但是调用者 msg.sender 必须是本合约。 function putCurEpochConPubKeyBytes(bytes memory _bytes) public { require(msg.sender == address(this), "Not Owner"); solved = true; } // 有漏洞，攻击者可以通过改变 _method 变量碰撞函数选择器，调用目标函数并完成攻击。 function executeCrossChainTx(bytes memory _method, bytes memory _bytes, bytes memory _bytes1, uint64 _num) public returns(bool success){ (success, ) = address(this).call(abi.encodePacked(bytes4(keccak256(abi.encodePacked(_method, "(bytes,bytes,uint64)"))), abi.encode(_bytes, _bytes1, _num))); } } 攻击方法 我们的目标是利用executeCrossChainTx()函数调用合约中的putCurEpochConPubKeyBytes()，目标函数的选择器为：0x41973cd9。观察到executeCrossChainTx()中是利用_method参数和"(bytes,bytes,uint64)"作为函数签名计算的选择器。因此，我们只需要选择恰当的_method，让这里算出的选择器等于0x41973cd9，通过选择器碰撞调用目标函数。 Poly Network黑客事件中，黑客碰撞出的_method为 f1121318093，即f1121318093(bytes,bytes,uint64)的哈希前4位也是0x41973cd9，可以成功的调用函数。接下来我们要做的就是将0x41973cd9转换为bytes类型：0x6631313231333138303933，然后作为参数输入到executeCrossChainTx()中。execu…

接收ETH receive和fallback
type: item
date: 2023-02-24T16:37:49+08:00
description: 回调函数 Solidity支持两种特殊的回调函数，receive()和fallback()，他们主要在两种情况下被使用： 接收ETH 处理合约中不存在的函数调用（代理合约proxy contract） 我们这一讲主要介绍接收ETH的情况。 接收ETH函数 receive receive()只用于处理接收ETH。一个合约最多有一个receive()函数，声明方式与一般函数不一样，不需要function关键字：receive() external payable { ... } receive()函数不能有任何的参数，不能返回任何值，必须包含external和payable。 当合约接收ETH的时候，receive()会被触发。receive()最好不要执行太多的逻辑因为如果别人用send和transfer方法发送ETH的话，gas会限制在2300，receive()太复杂可能会触发Out of Gas报错；如果用call就可以自定义gas执行更复杂的逻辑（这三种发送ETH的方法我们之后会讲到）。 我们可以在receive()里发送一个event，例如： // 定义事件 event Received(address Sender, uint Value); // 接收ETH时释放Received事件 receive() external payable { emit Received(msg.sender, msg.value); } 有些恶意合约，会在receive()函数嵌入恶意消耗gas的内容，使得一些退款合约不能正常工作：Akutar NFT项目因此被永久锁定了11539 ETH，接近2亿元！因此写包含退款等逻辑的合约时候，一定要注意这种情况。 回退函数 fallback fallback()函数会在调用合约不存在的函数时被触发。可用于接收ETH，也可以用于代理合约proxy contract。fallback()声明时不需要function关键字，必须由external修饰，一般也会用payable修饰，用于接收ETH:fallback() external payable { ... }。 我们定义一个fallback()函数，被触发时候会释放fallbackCalled事件，并输出msg.sender，msg.value和msg.data: // fallback fallback() external payable{ emit fallbackCalled(msg.sender, msg.value, msg.data); } receive和fallback的区别 receive和fallback都能够用于接收ETH，他们触发的规则如下： 触发fallback() 还是 receive()? 接收ETH | msg.data是空？ / \ 是 否 / \ receive()存在? fallback() / \ 是 否 / \ receive() fallback() 简单来说，合约接收ETH时，msg.data为空且存在receive()时，会触发receive()；msg.data不为空或不存在receive()时，会触发fallback()，此时fallback()必须为payable。 receive()和payable fallback()均不存在的时候，向合约发送ETH将会报错。 总结 这一讲，我介绍了Solidity中的两种特殊函数，receive()和fallback()，他们主要在两种情况下被使用，他们主要用于处理接收ETH和代理合约proxy contract。 转载：https://mirror.xyz/wtfacademy.eth/EroVZqHW1lfJFai3umiu4tb9r1ZbDVPOYC-puaZklAw

Solidity ABI编码解码
type: item
date: 2023-02-24T14:35:26+08:00
description: ABI (Application Binary Interface，应用二进制接口)是与以太坊智能合约交互的标准。数据基于他们的类型编码；并且由于编码后不包含类型信息，解码时需要注明它们的类型。 Solidity中，ABI编码有4个函数：abi.encode, abi.encodePacked, abi.encodeWithSignature, abi.encodeWithSelector。而ABI解码有1个函数：abi.decode，用于解码abi.encode的数据。这一讲，我们将学习如何使用这些函数。 ABI编码 我们将用编码4个变量，他们的类型分别是uint256, address, string, uint256[2]： uint x = 10; address addr = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71; string name = "0xAA"; uint[2] array = [5, 6]; abi.encode 将给定参数利用ABI规则编码。ABI被设计出来跟智能合约交互，他将每个参数转填充为32字节的数据，并拼接在一起。如果你要和合约交互，你要用的就是abi.encode。 function encode() public view returns(bytes memory result) { result = abi.encode(x, addr, name, array); } 编码的结果为 0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000 由于abi.encode将每个数据都填充为32字节，中间有很多0。 abi.encodePacked 将给定参数根据其所需最低空间编码。它类似 abi.encode，但是会把其中填充的很多0省略。比如，只用1字节来编码uint类型。当你想省空间，并且不与合约交互的时候，可以使用abi.encodePacked，例如算一些数据的hash时。 function encodePacked() public view returns(bytes memory result) { result = abi.encodePacked(x, addr, name, array); } 编码的结果为 0x000000000000000000000000000000000000000000000000000000000000000a7a58c0be72be218b41c608b7fe7c5bb630736c713078414100000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000006 由于abi.encodePacked对编码进行了压缩，长度比abi.encode短很多。 abi.encodeWithSignature 与abi.encode功能类似，只不过第一个参数为函数签名，比如"foo(uint256,address)"。当调用其他合约的时候可以使用。 function encodeWithSignature() public view returns(bytes memory result) { result = abi.encodeWithSignature("foo(uint256,address,string,uint256[2])", x, addr, name, array); } 编码的结果为 0xe87082f1000000000000000000000000000000000…

实例验证 - call delegatecall
type: item
date: 2023-02-24T14:16:00+08:00
description: 实例验证下call,delegatecall,两种方式下msg.sender和数据storage存储位置的区别 合约A pragma solidity ^0.8.19; contract A { address public temp1; uint256 public temp2; function three_call(address addr) public { (bool success, bytes memory result) = addr.call( abi.encodeWithSignature("test()") ); // 1 require(success, "The call to B contract failed"); } function three_delegatecall(address addr) public { (bool success, bytes memory result) = addr.delegatecall( abi.encodeWithSignature("test()") ); // 2 require(success, "The delegatecall to B contract failed"); } } 合约B pragma solidity ^0.8.19; contract B { address public temp1; uint256 public temp2; function test() public { temp1 = msg.sender; temp2 = 100; } } call 测试 B合约地址：0x086866663330344C7D1C51Bf19FF981AF3cB5782 A合约地址：0x05715D87C062B9685DD877d307b584bAbec964Ed 交易发起地址：0x6BC0E9C6a939f8f6d3413091738665aD1D7d2776 执行前数据 A B temp1 0x0000000000000000000000000000000000000000 0x0000000000000000000000000000000000000000 temp2 0 0 A合约执行three_call，参数为B合约地址 执行后数据 A B temp1 0x0000000000000000000000000000000000000000 0x05715D87C062B9685DD877d307b584bAbec964Ed temp2 0 100 delegatecall 测试 B合约地址：0x27153EDA2E085534811b040f6062f6528D6B80a1 A合约地址：0xd3C23F354Ca2160E6dC168564AB8954146cF35C9 交易发起地址：0x6BC0E9C6a939f8f6d3413091738665aD1D7d2776 执行前数据 A B temp1 0x0000000000000000000000000000000000000000 0x0000000000000000000000000000000000000000 temp2 0 0 A合约执行three_delegatecall，参数为B合约地址 执行后数据 A B temp1 address: 0x6BC0E9C6a939f8f6d3413091738665aD1D7d2776 0x0000000000000000000000000000000000000000 temp2 100 0 总结 A发起地址->B合约call->C合约 msg.sender为B合约地址,非原始A发起地址，数据保存在C合约中 A地址->B合约delegatecall->C合约 msg.sender为A发起地址，数据保存在B合约中 注意 callcode 已经在solidity 0.5+废弃，被delegatecall取代，故不做分析测试 TypeError: "callcode" has been deprecated in favour of "delegatecall". 参考 https://hicoldcat.com/posts/web3/senior-track-5/

实例验证 - 访问私有数据
type: item
date: 2023-02-24T08:51:30+08:00
description: 方案的具体讲解来自《智能合约安全审计入门篇 —— 访问私有数据》，本片文章将在QEasyWeb3测试链上进行数据验证 测试合约 contract Vault { uint256 public count = 123; address public owner = msg.sender; bool public isTrue = true; uint16 public u16 = 31; bytes32 private password; bytes32[3] public data; struct User { uint256 id; bytes32 password; } User[] private users; mapping(uint256 => User) private idToUser; constructor(bytes32 _password) { password = _password; } function addUser(bytes32 _password) public { User memory user = User({id: users.length, password: _password}); users.push(user); idToUser[user.id] = user; } } 部署合约 通过https://remix.ethereum.org/ 进行部署，具体操作不做详细讲解 网络配置 QEasyWeb3的网络配置，可以查看《QEasyChain 测试链信息》 类型 RPC https https://qeasyweb3.com http http://qeasyweb3.com chain id: 9528 合约操作 合约部署时，构造函数参数bytes32 _password，需要传入初始化参数，用于写入测试状态变量password 例如：0x4141414242424343430000000000000000000000000000000000000000000001 然后通过addUser再添加两个测试数据 0x4141414242424343430000000000000000000000000000000000000000000002 0x4141414242424343430000000000000000000000000000000000000000000003 执行完成后，开始slot的读取测试。 读取私有slot数据 为了简化操作，我们直接使用python3 + web3py进行测试脚本的编写, 也可以选用其他web3库，大同小异 我们上面合约部署完的合约地址是0x4398BdBD9eF8bcACc2A41Abc671BF8f428BB4904 测试脚本 import time from web3 import Web3 w3 = Web3(Web3.HTTPProvider('http://qeasyweb3.com')) if __name__ == "__main__": for i in range(0,7): print("slot"+str(i)+" = " + w3.toHex(w3.eth.getStorageAt("0x4398BdBD9eF8bcACc2A41Abc671BF8f428BB4904",hex(i)))) 执行后返回数据如下 slot0 = 0x000000000000000000000000000000000000000000000000000000000000007b slot1 = 0x000000000000000000001f016bc0e9c6a939f8f6d3413091738665ad1d7d2776 slot2 = 0x4141414242424343430000000000000000000000000000000000000000000001 slot3 = 0x0000000000000000000000000000000000000000000000000000000000000000 slot4 = 0x0000000000000000000000000000000000000000000000000000000000000000 slot5 = 0x0000000000000000000000000000000000000000000000000000000000000000 slot6 = 0x0000000000000000000000000000000000000000000000000000000000000002 slot0 存储的为状态变量count的值，等于123的十六进制 slo…

智能合约安全审计入门篇 —— 访问私有数据
type: item
date: 2023-02-23T17:01:00+08:00
description: 了解如何访问合约中的私有数据（private 数据） 前置知识 我们先来了解一下 solidity 中的三种数据存储方式： 1. storage（存储） storage 中的数据被永久存储。其以键值对的形式存储在 slot 插槽中。 storage 中的数据会被写在区块链中（因此它们会更改状态），这就是为什么使用存储非常昂贵的原因。 占用 256 位插槽的 gas 成本为 20,000 gas。 修改 storage 的值将花费 5,000 gas 。 清理存储插槽时（即将非零字节设置为零），将退还一定量的 gas 。 storage 共有 2^256 个插槽，每个插槽 32 个字节数据按声明顺序依次存储，数据将会从每个插槽的右边开始存储，如果相邻变量适合单个 32 字节，然后它们被打包到同一个插槽中否则将会启用新的插槽来存储。 storage 中的数组的存储方式就比较独特了，首先，solidity 中的数组分为两种： a.定长数组（长度固定）： 定长数组中的每个元素都会有一个独立的插槽来存储。以一个含有三个 uint64 元素的定长数组为例，下图可以清楚的看出其存储方式： b.变长数组（长度随元素的数量而改变）： 变长数组的存储方式就很奇特，在遇到变长数组时，会先启用一个新的插槽 slotA 用来存储数组的长度，其数据存储在另外的编号为 slotV 的插槽中。slotA 表示变长数组声明的位置，用 length 表示变长数组的长度，用 slotV 表示变长数组数据存储的位置，用 value 表示变长数组某个数据的值，用 index 表示 value 对应的索引下标，则length = sload(slotA) slotV = keccak256(slotA) + index value = sload(slotV) 变长数组在编译期间无法知道数组的长度，没办法提前预留存储空间，所以 Solidity 就用 slotA 位置存储了变长数组的长度。 我们写一个简单的例子来验证上面描述的变长数组的存储方式： pragma solidity ^0.8.0; contract haha{ uint[] user; function addUser(uint a) public returns (bytes memory){ user.push(a); return abi.encode(user); } } 部署这个合约后调用 addUser 函数并传入参数 a = 998，debug 后可以看出变长数组的存储方式： 其中第一个插槽为（这里存储的是变长数组的长度）： 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563 这个值等于： sha3("0x0000000000000000000000000000000000000000000000000000000000000000") key = 0 这是当前插槽的编号 value = 1 这说明变长数组 user[] 中只有一条数据也就是数组长度为 1 ; 第二个插槽为（这里存储的是变长数组中的数据）： 0x510e4e770828ddbf7f7b00ab00a9f6adaf81c0dc9cc85f1f8249c256942d61d9 这个值等于： sha3("0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563") 插槽编号为： key=0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563 这个值等于： sha3("0x0000000000000000000000000000000000000000000000000000000000000000")+0 插槽中存储的数据为： value=0x00000000000000000000000000000000000000000000000000000000000003e6 也就是 16 进制表示的 998 ，也就是我们传入的 a 的值。 为了更准确的验证我们再调用一次 addUser 函数并传入 a=999 可以得到下面的结果： 这里我们可以看到新的插槽为： 0x6c13d8c1c5df666ea9ca2a428504a3776c8ca01021c3a1524ca7d765f600979a 这个值等于： sha3("0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e564") 插槽编号为： key=0x290decd9548b6…