以太坊改进提案EIP-7702：EOA账户代码委托EIP-7702是一项以太坊改进提案，它允许外部拥有账户(EOA)将其代码委托给另一个合约地址，从而使EOA能够像智能合约一样执行代码。这项提案为以太坊生态系统带来了新的可能性，让用户账户能够拥有更多的功能性。EIP-7702的核心概念EIP-7702引入了一种新的交易类型（0x4），它扩展了现有的EIP-1559交易格式，增加了一个authorization_list字段。这个字段包含了授权信息，允许EOA账户将其代码委托给指定的合约地址。授权列表结构授权信息的结构包含以下字段：chain_id: 链IDaddress: 委托的合约地址nonce: 账户的nonce值y_parity, r, s: 签名信息Gas成本EIP-7702的gas成本计算遵循以下规则：基础gas成本继承自EIP-1559授权列表中的每个授权项需要额外支付25000gas如果在授权时EOA账户已经在access_list中，则返还一半的gas代码委托机制当EOA账户委托其代码给另一个合约地址时：EOA账户的代码会被设置为委托指示器：0xef0100 + 委托合约地址当对EOA账户进行call调用时，会加载委托合约的代码作为EOA账户的代码执行CODESIZE和CODECOPY指令使用的是委托合约的代码EXTCODESIZE和EXTCODECOPY指令使用的是EOA账户的代码（23字节）特殊情况处理EIP-7702对一些特殊情况有明确的处理规则：如果委托地址为零地址，则EOA账户的代码会被置空如果委托形成循环（委托指示器指向另一个委托），则使用第一个委托的代码如果委托地址为预编译合约，则相当于使用空代码，不会调用预编译合约函数交易执行流程EIP-7702交易的执行流程如下：交易检查和gas预付发送账户的nonce增加处理授权列表（在交易开始执行之前，增加EOA的nonce，设置EOA账户的代码为委托指示器）执行交易的call调用需要注意的是：如果设置EOA账户代码的交易执行失败（开始执行前就已经设置code了），则已经成功设置的账户代码不会被撤销如果处理某个授权时失败，则停止处理当前的授权，并继续处理下一个授权处理失败的授权不会退还gas费实际应用场景EIP-7702为以太坊生态系统带来了多种新的应用场景：1. EOA账户功能扩展通过代码委托，EOA账户可以拥有智能合约的功能，同时保持EOA的特性。这使得用户可以在不创建新合约的情况下，为自己的账户添加自定义逻辑。2. 账户抽象EIP-7702为账户抽象提供了新的可能性，允许用户自定义其账户的行为，例如实现多签名验证、社交恢复等功能。3. 资产托管在正确设置委托代码的之后，允许其他账户在授权的情况下代表EOA执行资产转账等操作。技术实现细节EIP-7702的实现涉及以太坊客户端的多个组件：交易处理：增加新的交易类型，并在交易执行前处理授权列表（nonce增加之后）代码加载：修改EVM以支持代码委托机制，并在call类调用时加载委托合约的代码签名验证是EIP-7702的关键部分，每个委托授权都需要对应的EOA亲自签署，授权信息的签名消息格式为：msg = (0x05 || rlp([chain_id, address, nonce]))而交易的签名消息格式为：msg = (0x04 || rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, amount, data, access_list, authorization_list]))参考链接提案标准
什么是Anchor？Rust通常被描述为Solana程序开发的通用语言。但更准确地说，Anchor才是真正的通用框架，因为大多数Rust开发都使用这个框架。Anchor是一个有主见且强大的框架，旨在快速构建安全的Solana程序。它通过减少账户（反）序列化和指令数据等领域的样板代码，进行必要的安全检查，自动生成客户端库，并提供广泛的测试环境，从而简化开发过程。本文将探讨如何开发Anchor程序。它涵盖了安装Anchor、使用Solana Playground以及创建、构建和部署一个简单的Hello, World!程序。然后，我们将深入探讨Anchor如何通过检查IDL、宏、Anchor程序的结构、账户类型和约束条件以及错误处理来简化开发过程。我们还将简要介绍跨程序调用和程序派生地址。本文将提供你开始使用Anchor所需的一切知识。前提知识本文假设你已了解Solana的编程模型。如果你是Solana开发新手，我建议阅读我之前的博客文章《Solana编程模型：Solana开发入门》。如果你是Rust新手，不必担心 - 开始Anchor开发不需要高级知识。Anchor文档指出，开发者只需熟悉Rust的基础知识（即Rust书籍的前九章）。我建议观看《Rust生存指南》，了解Rust编程概念的基本要点。了解Rust的内存、所有权和借用规则也至关重要。为了减轻学习曲线，我建议新接触低级编程语言的开发者复习Rust资源往往跳过的不同系统编程特定概念。例如，我建议了解变量大小、指针和内存泄漏等主题。我还推荐《通过示例学Rust》和我的仓库，其中包含用Rust编写的各种数据结构和算法示例。想使用TypeScript代替吗？了解如何使用Poseidon框架用TypeScript编写Solana程序，将TypeScript转译为Rust并生成有效的Anchor程序。本文专注于Anchor开发，仅限于Anchor开发。我们不会介绍如何用原生Rust开发程序，本文也不假设你了解这方面的知识。此外，本文不会涵盖Anchor的客户端开发 - 我们将在未来的文章中介绍如何通过TypeScript测试和与Anchor程序交互。话不多说，让我们开始使用Anchor吧！安装Anchor设置Anchor涉及几个简单的步骤，安装必要的工具和包。本节介绍如何安装这些工具和包（即Rust、Solana工具套件、Yarn和Anchor版本管理器）。安装RustRust可以从官方Rust网站安装，或通过命令行安装：curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh安装Solana工具套件Anchor还需要Solana工具套件。最新版本可以使用以下命令为macOS和Linux安装：sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"可以将 stable 替换为与您所需版本的软件版本匹配的发布标签（例如 v2.0.3），或者使用以下三个符号通道名称之一： stable、 beta 或 edge。安装后，运行solana --version确认已安装所需版本的solana。安装AVMAnchor 版本管理器 (AVM) 允许您在系统上安装和管理不同的 Anchor 版本，并在未来轻松更新 Anchor 版本。使用以下命令安装AVM：cargo install --git https://github.com/coral-xyz/anchor avm --force使用AVM安装AnchorAnchor文档建议通过Anchor版本管理器（AVM）安装Anchor。AVM简化了管理和选择anchor-cli二进制文件的多个安装。这可能是生成可验证构建或在不同程序之间使用替代版本所必需的。使用Solana PlaygroundSolana Playground是一个基于Web的IDE，允许开发者在浏览器中编写、构建和部署Solana程序。它是一个强大的工具，特别是对于那些不想在本地设置开发环境的开发者。创建、构建和部署Hello, World!程序这是因为它们的简单性，各种技能水平的开发者都能理解它们。这些程序还阐明了新编程模型的基本结构和语法，而不引入复杂的逻辑或函数。它已经迅速成为编码中相当标准的初学者程序，所以我们自然要为Anchor编写一个。本节介绍如何使用本地Anchor设置以及Solana Playground构建和部署Hello, World!程序。declare_id!宏为程序设置公钥。对于本地开发，用于设置程序的anchor init命令将在target/deploy目录中生成一个密钥对，并填充此宏。Solana Playground也会自动为我们完成这项工作。恭喜！你刚刚通过Solana Playground将第一个Solana程序部署到devnet！Anchor如何简化开发过程Anchor通过有效抽象简化程序开发。也就是说，Anchor简化了复杂的区块链编程概念，使它们更容易访问和使用。IDL（接口描述语言）开发者可以像通常使用任何典型的Rust数据结构那样定义状态，然后Anchor处理底层序列化和存储到账户中。在链上发布IDL也非常简单。开发者可以使用以下命令发布IDL：anchor idl publish --provider.cluster devnet --provider.wallet ~/.config/solana/id.json <program-id>确保提供的钱包是程序的权限，并且有足够的SOL用于交易。开发者现在可以在区块浏览器（如XRAY或Solana Explorer）上查看他们的IDL。宏Anchor的宏是最重要的抽象之一。它们允许开发者编写类似于match表达式的内容，通过macro_rules!构造。过程宏更像是一个函数，接受一些代码作为输入，对该代码进行操作，并产生一些输出。在Anchor中，例如，#[account]宏定义并强制执行Solana账户的约束条件。Anchor程序结构Anchor的程序结构旨在利用宏和特性的组合来生成样板代码并强制执行程序逻辑。这种设计理念在简化开发过程和确保程序行为的一致性和可靠性方面发挥着重要作用。账户类型和约束条件在未来的文章中，我们将更深入地介绍Solana程序安全性和黑客攻击Anchor程序。然而，在这里介绍约束条件很重要。约束条件允许开发者验证某些账户或它们持有的数据是否符合一些预定义的要求。可以使用#[account(...)]属性应用几种不同类型的约束条件，该属性也可以引用其他数据结构。程序派生地址（PDA）来源：一个基本的Ping计数器程序示例，由Solana基金会提供。记住，PDA是曲线外的，没有关联的私钥。它们允许程序签署指令，并允许开发者在链上构建类似哈希映射的结构。PDA是使用可选种子列表、碰撞种子和程序ID派生的。重申一下，以下约束条件用于检查给定账户是否是从当前执行的程序、种子以及（如果提供）碰撞派生的PDA：错误处理Anchor还将尝试在可能的情况下解析程序错误堆栈，以追溯到程序错误的原始原因。ProgramError作为基础错误类型，其实用性通过Anchor的日志记录和解析机制得到增强，以提供详细的错误信息。跨程序调用（CPI）跨程序调用（CPI）在整篇文章中都有提及，所以我们有一个专门的章节来讨论它们是合适的。CPI对Solana的可组合性至关重要，因为它们促进了程序直接调用其他程序。这将Solana生态系统变成了一个庞大的、相互连接的API，供开发者使用。为了简洁起见，我建议阅读Anchor关于CPI的文档，因为它们提供了一个有用的CPI实际应用示例，包括傀儡和傀儡主人程序。总结Anchor是一个强大的框架，一点也不为过。我们探索了Anchor用于减少代码量的各种宏和特性，它简化开发流程的能力显而易见。它拥有完善的文档和强大的相关教程及crate生态系统。Anchor深受绝大多数Solana开发者的喜爱和使用。anchor指南
Solana的重要概念：共识机制、账户模型、程序、交易、指令等等一、共识机制Solana的创新共识机制Proof of History (PoH)PoH是Solana区块链的核心创新之一，它为区块链提供了一个去中心化的时间源。PoH本质上是一个高频可验证延迟函数 (VDF)，它能够创建历史记录，证明某个事件发生在特定时刻之前或之后。PoH是一种创新的时间记录机制，通过加密散列函数生成一个可验证的时间顺序，使得区块链中的事件可以按时间线性顺序排列。这减少了节点之间的时间同步需求，使得节点能够迅速达成共识，而无需等待整个网络确认。PoH工作流程：使用哈希函数将事件序列化，每个哈希值都依赖于前一个哈希值通过计数器记录哈希计算的次数，可以推断出时间的流逝当需要记录事件时，将事件数据插入到PoH序列中，后续所有哈希值都会因此变化这种机制确保了事件的顺序性和时间戳的可验证性Proof of Stake (PoS)验证者需要抵押一定数量的SOL代币，持有更多代币的验证者有更高的概率被选中生成区块。PoS提供了经济激励，确保验证者行为诚实。PoH与PoS的结合：PoH确保区块的时间戳和顺序，PoS则确保网络的安全性和抗攻击性，两者结合使得网络能够达到每秒数千笔交易处理速度。 虽然权益证明(PoS)确保网络中达成共识，但Solana将历史证明(PoH)纳入其PoS共识机制中，从而实现连续区块生产的同步。以太坊从PoW到PoS的转变以太坊 2.0 的升级（The Merge）将共识机制从能源密集的 PoW 转为 PoS，大幅降低了能耗。验证者通过锁定 ETH 参与共识，取代了矿工的角色，提升了网络的安全性和可持续性。Solana与以太坊共识机制的比较在非分区状态下，Solana网络中始终都只有一个领导者。每个验证器节点和领导者拥有相同的硬件能力，且验证器可以被选举为领导者，这种选举是基于PoS的。这种网络设计，在出块速度上相比于以太坊将会有大幅度提升，但是，在去中心化程度上，却不如以太坊。二、交易处理能力Solana的并行处理通过将交易分解为多个子集，利用不同的验证节点并行处理，提高了吞吐量。Solana的架构支持高达每秒数千笔交易，平均出块时间为400毫秒，平均每秒2000+笔交易，在高负载时保持较低的交易费用。低延迟和低费用Solana的架构设计使其能够快速确认交易，通常在一秒以内完成。即使在高负载下，交易费用仍保持低廉，适合需要高频交易的应用。海湾流（Gulf Stream）和海平面（Sealevel）技术Solana在技术底层架构上，不得不提到的两个技术层面一个就是海湾流（Gulf Stream)以及海平面（Sealvel)，这两个技术相辅相成，是支撑Solana成为新的世界级公链的两大支柱。海湾流(Gulf Stream)：无内存交易转发协议，解决了传统区块链内存池（MemPool）造成的网络瓶颈问题海平面(Sealevel)：允许智能合约并行运行的技术，大幅提高了交易处理效率以太坊的串行执行以太坊的智能合约在虚拟机（EVM）中按顺序执行，每笔交易必须等待前一笔完成才能开始。虽然这种方式确保了状态的一致性，但也限制了以太坊吞吐量长期维持在每秒15笔～30笔，以牺牲性能换来安全性和一致性。Layer 2和RollupLayer 2解决方案通过将大量交易在链下处理，然后将结果提交到主链，显著提高了交易速度和降低了成本。Rollup是其中一种常见的技术，能够将数百笔交易合并为一个批次，能够将燃料费减少多达100倍。三、交易费用（Gas费）Solana的交易费用机制Solana的交易费用是根据交易的复杂度和大小动态计算的，这意味着，交易费用会根据交易的执行成本而变化，而不是根据网络上的交易量变化。Solana的平均交易费用通常低于0.01美元，平均为0.00025美元，这使得进行小额交易更具成本效益。基础手续费结构基础手续费目前设定为每个签名0.000005 SOL（5,000 lamports），构成了交易成本的基础。这是由一个地址支付的费用，以便获得使用网络资源的权利。这是一个一次性的总费用，无论实际使用多少资源来执行交易（或者交易是否执行），都需要预先支付给网络。计算单元Solana交易会预先请求指定数量的计算单元（CUs），如果超出这个数量，交易将失败。这意味着开发者目前几乎没有任何财务激励来最小化计算单元的请求。以太坊的交易费用以太坊的交易费用因网络拥堵而波动，这是一种纯粹的市场机制，网络中交易拥堵情况下你的交易要想被确认，就需要支付高昂的手续费。目前一笔转账交易的手续费大概在 1 ～ 10 美元左右。四、智能合约Solana的程序模型Solana中一切皆账户，它的智能合约（Solana 中称之为程序program）也是账户，但细分为可执行账户和数据账户，前者存储程序的代码，用来执行特定的逻辑，后者存储状态，即程序运行时的数据。在 Solana 生态系统中，"智能合约"被称为程序。每个程序都是一个链上账户，存储可执行逻辑，组织成特定的函数，称为指令(instructions)。程序的特点程序是包含可执行代码的链上账户代码组织成不同函数，称为指令程序可以由升级权限更新。当升级权限设置为 null 时，程序变为不可变可验证的构建使用户能够验证链上程序是否与公开的源代码相匹配程序开发方法Solana程序主要用Rust编程语言编写，开发有两种常见方法：Anchor：专为Solana程序开发设计的框架。它提供了一种更快、更简单的编写程序的方式，使用 Rust 宏来显著减少样板代码。对于初学者，建议从 Anchor 框架开始。原生Solana程序库：直接使用Solana程序的标准库solana_program进行开发这种分离的模式，使得程序的升级更加简单，因为程序本身无状态，可以直接升级为新的代码逻辑。以太坊的智能合约以太坊的智能合约本身就包含了合约的逻辑代码，以及状态数据。因此合约部署之后，就不支持直接的升级，只能通过代理的方式间接升级，即重新部署一套合约代码，生成新的合约地址，代理再指向这个新的合约地址。五、账户Solana的账户模型Solana中一切皆账户，它的账户就像一个容器（或者电脑中的文件夹），可以包含程序代码、状态数据以及账户元数据。账户类型按照功能可划分为可执行账户和数据账户：可执行账户：存储程序代码的账户，也称为程序账户数据账户：包括普通用户账户和其他非程序账户，这些账户存储了用户的余额、交易历史和其他相关数据，但它们本身不包含程序代码账户结构每个账户都有一个地址（一般情况下是一个公钥）以及一个所有者（程序账户的地址）。账户存储的完整字段包括：地址（公钥）所有者（程序账户的地址）余额数据（可以是程序代码或状态数据）是否可执行租金状态PDA账户PDA（Program Derived Address）账户，也就是程序的派生账户，是Solana中一种特殊的账户类型，允许程序在没有私钥的情况下控制账户。对于熟悉以太坊账户的同学来说，这种划分方式也许有点奇怪，不过随着了解的深入，就会认识到 Solana 的单一账户模型，使得多个交易能够并行处理，这正是 Solana 高性能的基础。以太坊的账户模型以太坊分为EOA账户和智能合约，前者是普通用户在以太坊网络中的账户，用于存储以太币（ETH）和进行交易。后者是包含智能合约代码和状态的账户，这些账户由合约创建并部署在以太坊区块链上。通过这两种账户类型的结合，以太坊提供了一个灵活且功能强大的去中心化应用开发平台。六、交易与指令交易结构在Solana中，交易是由一组指令组成的，每个指令包含以下内容：program_id：程序是基于账户来运行的，被标记为"可执行的账户"会分配给BPF Loader。此帐户的地址称为'program_id'，该地址将用于在所有的未来交易中引用程序accounts：存放着读取和写入的账户地址数组instruction_data：与关联的程序定义并解析指令（Instructions）指令（Instructions）是Solana中最基本的操作单元。在Solana程序中，指令处理函数通常返回ProgramResult类型，它是solana_program中的一个结构体，代表着Solana程序中指令处理函数的返回值。该类型代表Transaction交易中指令的处理结果，成功时为单元类型()，即返回值为空，失败时返回值为ProgramError。交易执行流程创建交易：组装一组指令，指定需要操作的账户和程序签名交易：使用私钥对交易进行签名提交交易：将签名后的交易发送到网络验证与执行：验证者节点验证交易签名并执行指令确认交易：交易被打包进区块并获得确认七、租金机制Solana的租金Solana的租金（也称为交易费用或gas费用）是根据执行智能合约或进行其他操作所需的计算和存储资源的量来计算的。租金计算因素通常来说，Solana的租金由以下几个因素决定：程序执行的复杂性：如果智能合约需要更多的计算资源来执行，那么相应的租金将更高账户大小：账户占用的存储空间越大，需要支付的租金越高租金豁免：如果账户余额超过两年租金的最低余额，则可以免交租金租金收取周期Solana链上程序的部署是按其账户大小进行定期收费的，目前的费用是2天一收，1K大小约0.01 SOL。如果账户无法支付租金，系统将清除该账号。为了防止账户被删除，用户必须支付租金，也就是存储费。如果用户钱包中持有相当于两年租金的最低余额，那么这个账户可以免交租金。八、Solana的优势与挑战优势高吞吐量：Solana的架构支持高达每秒数千笔交易，远超大多数区块链低交易费用：即使在高负载下，交易费用仍保持低廉，适合需要高频交易的应用创新的共识机制：PoH与PoS的结合提供了高效的时间同步和网络安全程序升级灵活性：程序与数据分离的设计使得升级更加简单挑战网络稳定性：Solana曾经历多次网络宕机，2021年9月14日，Solana的网络崩溃，脱机17小时去中心化程度：为了追求高性能，Solana在去中心化程度上不如以太坊生态系统规模：相比以太坊，Solana的DApp生态系统仍在发展中，根据数据，在Solana上构建了350多个DAPP，相比之下，以太坊上有近3000个DAPP总结Solana通过创新的技术架构和设计理念，为区块链世界带来了高性能、低成本的解决方案。它的PoH共识机制、账户模型、程序设计和交易处理方式都与传统区块链有着显著差异，这些差异使得Solana能够实现每秒数千笔交易的处理能力，同时保持较低的交易费用。虽然Solana在去中心化程度和网络稳定性方面仍面临挑战，但其技术创新和性能优势使其成为区块链开发者的重要选择，特别是对于需要高吞吐量和低延迟的应用场景。随着技术的不断完善和生态系统的发展，Solana有望在区块链领域发挥更加重要的作用。正如Solana的账户模型所体现的那样，Solana的账户模型不仅仅是一种存储机制，更是一种控制执行、构建权限边界、提升性能的系统设计哲学。这种设计哲学贯穿了Solana的整个技术架构，使其成为一个独特而强大的区块链平台。参考文章：Solana基础概念
引言在区块链技术的快速发展中，安全问题始终是一个不可忽视的关键因素。其中，重放攻击作为一种常见的攻击手段，对区块链交易的安全构成了重大威胁。本文将深入探讨区块链重放攻击的概念、常见类型以及有效的防护措施，帮助开发者和用户更好地理解并防范这类安全风险。什么是区块链重放攻击？重放攻击是一种攻击手段，攻击者通过拦截并操作网络上的数据传输来实现。在区块链领域，重放攻击指攻击者通过操控并重用有效交易来获取不当利益。具体而言，攻击者可以将交易复制到另一个链上，或者复制、修改签名，使其通过验证检查，从而实现非法目的。在理解重放攻击之前，建议先了解以下基础知识：以太坊中的签名和认证机制ECDSA密钥生成、签名和验证过程EIP-191和EIP-712签名标准五种常见的区块链重放攻击类型1. 缺失应用程序随机数(Nonce)重放攻击攻击原理签名是区块链技术中提供加密认证的关键机制，作为唯一的"指纹"构成区块链交易的基础。在这种攻击中，由于缺少应用程序随机数(Nonce)，攻击者可以重复使用之前的有效签名进行欺诈交易。攻击流程签名者签署一个不包含随机数的消息攻击者从之前的交易中拦截并复制签名攻击者重新利用此签名发送新的交易防护措施实现应用程序随机数是防止这类攻击的有效方法。在智能合约中，可以通过以下方式实现：// 在合约中添加随机数映射 mapping (address => uint256) public nonces; // 在签名验证过程中检查随机数 function verifySignature(address signer, uint256 nonce, bytes memory signature) internal { // 验证随机数是否递增 require(nonce == nonces[signer], "Invalid nonce"); // 验证签名... // 更新随机数 nonces[signer]++; }2. 硬分叉重放攻击攻击原理硬分叉是对区块链协议的重大且不向后兼容的更改，导致创建两个独立账本：原始账本和新分叉账本。当两个账本共享相同的交易和地址格式时，就会出现重放攻击的可能性。攻击流程区块链发生硬分叉，形成两条独立的链攻击者拦截一条链上的合法交易攻击者将相同的交易重放到另一条链上防护措施实现链ID验证：在交易签名中包含链ID，确保交易只能在特定链上有效使用不同的地址格式：为新链设计不同的地址格式，防止交易在不同链间重放实现重放保护机制：在硬分叉时添加特殊的交易字段，使交易只能在一条链上有效3. 跨链重放攻击攻击原理随着区块链生态系统的扩展，不同链之间的互操作性增强，这也带来了跨链重放攻击的风险。攻击者可以将在一条链上有效的交易重放到另一条链上。攻击流程用户在链A上签署交易攻击者截获该交易攻击者将相同的交易提交到链B上防护措施在签名消息中包含链标识符实现跨链通信协议，确保交易的唯一性使用不同的签名算法或参数4. 签名可塑性重放攻击攻击原理签名可塑性是指在不改变签名有效性的情况下，可以修改签名的特性。攻击者可以利用这一特性，修改交易签名并重新广播，导致同一交易被多次执行。攻击流程用户发送有效交易攻击者修改签名（保持其有效性）攻击者重新广播修改后的交易防护措施使用标准化的签名格式（如低S值）实现交易ID计算方法，使其不受签名可塑性影响在智能合约中实现额外的验证机制5. 密码学随机数重用重放攻击攻击原理在ECDSA等签名算法中，如果在不同消息的签名过程中重用了相同的密码学随机数(k)，攻击者可以计算出私钥，从而完全控制账户。攻击流程签名者在两个不同消息的签名中使用相同的随机数k攻击者获取这两个签名攻击者通过数学计算推导出私钥防护措施使用确定性签名算法（如RFC 6979）确保每次签名使用不同的随机数使用硬件安全模块(HSM)进行签名操作综合防护策略1. 实现完整的签名验证在智能合约中实现严格的签名验证机制，包括：验证签名者地址检查消息内容完整性验证随机数的有效性和唯一性检查签名的有效期2. 使用标准化的签名方案采用经过验证的签名标准，如EIP-712，它提供了结构化数据的签名方案，使签名更安全、更易于验证。3. 实现交易唯一性保障通过以下方式确保交易的唯一性：在交易中包含时间戳使用递增的随机数添加唯一的交易标识符4. 定期安全审计对智能合约和区块链应用进行定期安全审计，特别关注与签名验证和交易处理相关的代码。实际案例分析KYCRegistry合约中的重放攻击漏洞以下是一个存在重放攻击漏洞的KYCRegistry合约示例（点击查看更多详情）：function addKYCAddressViaSignature(uint8 kycRequirementGroup, address user, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external { require(v == 27 || v == 28, "KYCRegistry: 签名中的无效 v 值"); require( !kycState[kycRequirementGroup][user], "KYCRegistry: 用户已验证" ); require(block.timestamp <= deadline, "KYCRegistry: 签名已过期"); bytes32 structHash = keccak256( abi.encode(_APPROVAL_TYPEHASH, kycRequirementGroup, user, deadline) ); // @audit - 需要在这里编码 nonce 以防止重播 bytes32 expectedMessage = _hashTypedDataV4(structHash); address signer = ECDSA.recover(expectedMessage, v, r, s); _checkRole(kycGroupRoles[kycRequirementGroup], signer); kycState[kycRequirementGroup][user] = true; // ... }修复后的代码// 添加随机数映射 mapping(address => uint256) public nonces; function addKYCAddressViaSignature(uint8 kycRequirementGroup, address user, uint256 deadline, uint256 nonce, uint8 v, bytes32 r, bytes32 s) external { require(v == 27 || v == 28, "KYCRegistry: 签名中的无效 v 值"); require( !kycState[kycRequirementGroup][user], "KYCRegistry: 用户已验证" ); require(block.timestamp <= deadline, "KYCRegistry: 签名已过期"); require(nonce == nonces[user], "KYCRegistry: 无效随机数"); bytes32 structHash = keccak256( abi.encode(_APPROVAL_TYPEHASH, kycRequirementGroup, user, deadline, nonce) ); bytes32 expectedMessage = _hashTypedDataV4(structHash); address signer = ECDSA.recover(expectedMessage, v, r, s); _checkRole(kycGroupRoles[kycRequirementGroup], signer); // 更新随机数 nonces[user]++; kycState[kycRequirementGroup][user] = true; // ... }结论区块链重放攻击是一种常见但危险的安全威胁，了解其不同类型和防护措施对于开发安全的区块链应用至关重要。通过实现适当的随机数机制、签名验证和交易唯一性保障，可以有效防止重放攻击。随着区块链技术的不断发展，安全意识和最佳实践的应用将继续成为保障区块链生态系统健康发展的关键因素。开发者应当时刻关注最新的安全研究和防护技术，确保其应用能够抵御各种形式的攻击。参考资料EIP-155：防止跨链重放攻击EIP-191: 签名数据标准EIP-712: 结构化数据的类型化签名
引言本文详细描述了调用智能合约时可能发生的各种错误，以及Solidity的Try/Catch块如何响应（或未能响应）这些错误。要理解Solidity中的Try/Catch如何工作，我们必须了解当低级调用失败时返回的数据。编译器决定了这种行为，而不是以太坊虚拟机。因此，用其他语言或汇编编写的合约不一定会遵循这里解释的所有错误格式。低级调用失败的情况当对外部合约的低级调用失败时，它返回一个布尔值false。这个false表示调用未成功执行。调用在以下情况下可能返回false：被调用的合约回滚（revert）被调用的合约执行非法操作（如除以零或访问越界数组索引）被调用的合约耗尽所有gas在以下部分中，我们将检查可能导致低级调用返回 false 的情况，以及它们可能提供的任何返回数据。然后我们将探讨Try/Catch如何处理（或未能处理）每种情况。第1部分：在回滚时返回什么内容1. 如果使用了没有错误字符串的revert，会返回什么？使用revert的最简单方法是不提供回滚原因。contract ContractA { function mint() external pure { revert(); } }如果我们部署上述合约（ContractA）并从另一个合约（ContractB）进行低级调用mint()函数，如下所示：// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; import "hardhat/console.sol"; contract ContractB { function call_failure(address contractAAddress) external { (, bytes memory err) = contractAAddress.call( abi.encodeWithSignature("mint()") ); console.logBytes(err); } }revert()错误将被触发，不会返回任何数据。错误返回的数据是0x，这只是没有附带数据的十六进制表示。2. 从带有错误字符串的revert中返回什么？另一种使用revert的方法是提供一个字符串消息。这有助于识别合约中交易失败的原因。让我们触发一个带有字符串的回滚，看看返回了什么：contract ContractB { function mint() external pure { revert("Unauthorized"); } }调用合约将是：import "hardhat/console.sol"; contract ContractA { function call_failure(address contractBAddress) external { (, bytes memory err) = contractBAddress.call( abi.encodeWithSignature("mint()") ); console.logBytes(err); // just so we can see the error data } }当revert带有字符串参数触发时，它会将 Error 函数Error(string)的ABI编码返回给调用者。我们回滚的返回数据将是函数调用的ABI编码Error("Unauthorized")。在这种情况下，它将具有Error(string)函数的函数选择器，字符串的偏移量，长度和字符串内容的十六进制编码。让我们进一步解释输出：选择器08c379a0是keccak256("Error(string)")的前四个字节，其中string指的是原因字符串。接下来的96个字节（3个32字节）是字符串Unauthorized的ABI编码前32个字节是字符串长度位置的偏移量第二个32个字节是字符串的长度（12个字节以十六进制表示为c）字符串Unauthorized的实际内容以UTF-8编码为字节556e617574686f72697a65643. 从自定义revert中返回什么？Solidity0.8.4引入了错误类型，可以与revert语句一起使用，以创建既可读又节省gas的自定义错误。要创建自定义错误类型，你将使用关键字error来定义错误，类似于定义事件：error Unauthorized();如果需要强调错误信息的一些细节，你也可以定义带有参数的自定义错误：error CustomError(arg1, arg2, etc)。error Unauthorized(address caller);没有参数的自定义revert让我们比较一个带参数的自定义回滚与一个不带参数的例子：pragma solidity >=0.8.4; error Unauthorized(); contract ContractA { function mint() external pure { revert Unauthorized(); } }在上述例子中，我们希望回滚交易并返回错误Unauthorized。我们的调用合约将保持不变：import "hardhat/console.sol"; contract ContractB { function call_failure(address contractAAddress) external { (, bytes memory err) = contractAAddress.call( abi.encodeWithSignature("mint()") ); console.logBytes(err); // just so we can see the error data } }不带参数的自定义回滚将仅返回函数选择器（keccak256("Unauthorized()")的前四个字节）给调用者，即0x82b42900。带参数的自定义revert现在让我们看看带参数的自定义错误：pragma solidity >=0.8.4; error Unauthorized(address caller); contract ContractA { function mint() external pure { revert Unauthorized(msg.sender); } }0x8e4a23d60000000000000000000000009c84abe0d64a1a27fc82821f88adae290eab5e07带参数的自定义回滚将返回函数选择器（keccak256("Unauthorized(address)")的前四个字节）以及地址参数的ABI编码。在这种情况下，返回的数据将包含函数选择器和调用者地址的编码。这里需要注意的是，你不能定义自定义错误error Error(string)或error Panic(uint256)，因为这些与require和assert分别返回的错误冲突第2部分：Try/Catch如何处理错误Solidity中的Try/Catch基础Solidity 0.6版本引入了try/catch语句，这是错误处理能力的一大飞跃。try/catch允许我们处理外部函数调用失败的情况，而不需要回滚整个交易（被调用函数中的状态更改仍会回滚，但调用函数中的状态更改不会）。在Solidity中，try/catch只能用于：外部合约调用合约创建调用（constructor被视为external函数）基本语法如下：try externalContract.f() { // 调用成功的情况下执行的代码 } catch { // 调用失败的情况下执行的代码 }如果调用的函数有返回值，必须在try后面声明变量来接收：try externalContract.f() returns (uint256 value) { // 使用返回的value值 } catch { // 处理错误 }捕获特定类型的错误Solidity的try/catch提供了四种捕获错误的方式：捕获带有原因的错误：使用catch Error(string memory reason) {}可以捕获通过revert("reason")或require(false, "reason")抛出的错误。try externalContract.f() { // 成功代码 } catch Error(string memory reason) { // 处理带有原因的错误 // 例如：reason = "Unauthorized" }捕获所有非法操作：使用catch Panic(uint256 errorCode) {}，例如除以零错误，以及assert错误等try externalContract.f() { // 成功代码 } catch Panic(uint256 errorCode) { // 处理非法操作错误 }捕获自定义错误：使用catch (bytes memory reason) {}或具体的错误定义catch Unknown(uint256 code) {}可以捕获自定义错误。try externalContract.f() { // 成功代码 } catch (bytes memory reason) { // 处理自定义错误 // 例如：reason = 自定义错误的ABI编码 }捕获所有错误：使用catch {}可以捕获所有其他类型的错误。try externalContract.f() { // 成功代码 } catch Error(string memory reason) { // 处理带有原因的错误 } catch { // 处理所有其他错误 }注：catch Error(string memory reason) {}和catch {}两种捕获错误的方式不能共存实际应用示例下面这个合约展示了如何使用try/catch处理合约创建可能失败的情况：pragma solidity >=0.7.0 <0.9.0; interface Other { function hello() external returns (bytes memory); } contract Contract { function callOtherContract(Other other) public returns (bytes memory){ try other.hello() returns (bytes memory data) { return data; } catch Panic(uint256 errorCode) { // 处理非法错误 } catch Error(string memory reason) { // 处理带有原因的错误 } catch (bytes memory reason) { // 处理所有其他错误 } } }第3部分：Try/Catch的限制和注意事项使用try/catch时需要注意以下几点：只能用于外部调用：try/catch只能用于外部合约调用和合约创建，不能用于内部函数调用。如果想在同一合约内使用try/catch，可以通过this.function()的方式调用，这会被视为外部调用。只捕获外部调用中的异常：try/catch只能捕获外部调用本身中发生的异常，不能捕获调用参数计算过程中的异常。例如：function createCharitySplitter(address _charityOwner) public { try new CharitySplitter(getCharityOwner(_charityOwner, false)) returns (CharitySplitter newCharitySplitter) { // 成功代码 } catch { // 错误处理 } } function getCharityOwner(address _charityOwner, bool _toPass) internal returns (address) { require(_toPass, "revert-required-for-testing"); // 这个错误不会被try/catch捕获 return _charityOwner; }不能在构造函数中使用this：不能在构造函数中使用this.f()，因为此时合约还未创建完成。状态回滚的范围：当外部调用失败时，该调用中的所有状态更改都会回滚，但调用前后的状态更改不会回滚。总结Solidity中的错误处理机制提供了多种方式来抛出和捕获错误：抛出错误的方式：使用revert()不带原因使用revert("reason")带原因使用自定义错误error ErrorName()不带参数使用自定义错误error ErrorName(param1, param2)带参数使用require(condition, "reason")使用assert(condition)捕获错误的方式：使用try/catch捕获外部调用中的错误使用catch Error(string memory reason)捕获带原因的错误使用catch (bytes memory reason)捕获自定义错误使用catch捕获所有其他错误通过合理使用这些错误处理机制，我们可以编写更加健壮和用户友好的智能合约，提高合约的可靠性和安全性。参考资料Solidity官方文档 - 错误处理Solidity官方文档 - Try/CatchSolidity 0.6.x features: try/catch statement
智能合约代理（Proxy）引言在区块链开发中，智能合约一旦部署到链上就无法更改，这种不可变性虽然提供了安全保障，但也带来了灵活性的挑战。代理模式应运而生，成为解决这一问题的关键技术。基础概念什么是代理模式？代理（Proxy）本身并不是固有可升级的，但它是几乎所有可升级合约模式的基础。对代理合约的调用会通过 delegatecall 转发到实现合约（也称为逻辑合约）。这种机制允许合约逻辑与存储分离，从而实现合约的可升级性。关键术语代理合约（Proxy Contract）：用户直接交互的合约，负责转发调用并存储数据实现合约/逻辑合约（Implementation/Logic Contract）：包含实际业务逻辑的合约delegatecall：特殊的调用方式，在调用者的上下文中执行被调用合约的代码存储槽（Storage Slot）：合约存储中的特定位置，用于存储变量基本代理模式EIP-1167最小代理EIP-1167 标准于 2018 年 6 月创建，旨在标准化以简单、便宜且不可变的方式克隆合约功能。该标准包含了针对代理合约优化的最小字节码重定向实现。通常与工厂模式一起使用。// 最小代理合约示例 bytes memory bytecode = hex"363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3"; // 其中 "bebebebe..." 需要替换为实际的实现合约地址 bytecode = abi.encodePacked( bytecode[:20], implementationAddress, bytecode[40:] );代码解读特点：实现地址：在代理合约中是不可变的升级逻辑：纯代理合约中没有可升级性合约验证：可与Etherscan和其他区块浏览器配合使用。优点：部署成本低缺点：每次调用会增加一次 delegatecall 的费用无法升级实现合约应用场景：当需要部署多个代码基本相同的合约时非常有用实际应用：Uniswap V1 AMM 池已知漏洞：实现合约中不允许使用 delegatecall 和 selfdestruct可初始化代理初始化的挑战使用代理的主要好处之一是只需部署一次实现合约，然后可以部署多个指向它的代理合约。然而，缺点是无法在创建新代理时使用已经部署的实现合约中的构造函数。正如开发者常问的："但我们该如何在没有 constructor() 的情况下工作？"初始化函数解决方案是使用 initialize() 函数来设置初始存储值：uint8 private _initialized; function initialize() external { require(msg.sender == owner); require(_initialized < 1); _initialized = 1; // 设置一些状态变量 // 做初始化工作 }优点：允许在新代理部署时设置初始存储缺点：易受与初始化相关的攻击，尤其是未初始化的代理应用场景：任何需要在代理合约部署时设置存储的代理，适用于大多数代理类型，包括TPP和UUPS。可升级代理基本可升级代理可升级代理类似于简单代理，只不过实现合约地址是可设置的，并保存在代理合约中。代理合约还包含授权的升级功能。特点：实现地址：位于代理存储中升级逻辑：位于代理合约中合约验证：根据具体实现，可能无法与像和Etherscan这样的区块浏览器配合使用优点：通过使用代理降低部署成本实现合约可以升级缺点：易受存储和功能冲突的影响安全性不如当前同类产品。每次调用都要承担来自代理的 delegatecall 费用应用场景：这种基本风格已不再广泛使用。已知漏洞：实现中不允许使用 delegatecall 和 selfdestruct未初始化的代理存储冲突功能冲突EIP-1967 可升级代理这类似于基本可升级代理，但通过使用无结构存储模式减少存储冲突的风险。它不将实现合约地址存储在槽0或任何其他标准存储槽中。相反，地址存储在预先商定的槽中。例如，OpenZeppelin合约使用字符串"eip1967.proxy.implementation"的keccak256哈希值减去 1。由于这个槽广泛使用，区块浏览器可以识别并处理代理的使用。减去1提供了额外的安全性，因为没有它，存储槽有一个已知的预映像，但在减去1之后，预映像是未知的。对于已知的预映像，存储槽可能会通过映射被覆盖，例如，其中存储槽的键是通过keccak256哈希确定的。EIP-1967还指定了一个用于管理员存储（auth）的槽，以及Beacon代理模式。特点：实现地址：位于代理合约中的唯一存储槽升级逻辑：根据实现而有所不同高级代理模式透明代理模式（Transparent Proxy Pattern, TPP）透明代理模式是一种解决功能选择器冲突的方法。在这种模式中，代理合约会检查调用者的地址。如果调用者是管理员，则调用代理的管理函数；如果调用者是其他地址，则将调用转发到实现合约。modifier ifAdmin() { if (msg.sender == _getAdmin()) { _; } else { _fallback(); // redirects call to proxy } }优点：解决了功能选择器冲突问题安全性较高缺点：每次调用都需要额外的gas来检查调用者是否是管理员管理员无法直接调用实现合约的函数应用场景：需要高安全性的可升级合约实际应用：许多DeFi项目，如USDCUUPS（Universal Upgradeable Proxy Standard）UUPS 是一种将升级逻辑放在实现合约中而不是代理合约中的模式。这样，代理合约可以更加轻量级，并且不需要检查调用者是否是管理员。// 实现合约中的升级函数 function upgradeToAndCall(address newImplementation, bytes memory data) internal { ... }优点：代理合约更加轻量级每次调用的gas成本更低缺点：实现合约必须包含升级逻辑如果忘记在新实现中包含升级逻辑，可能会导致合约无法再次升级应用场景：需要优化gas成本的可升级合约实际应用：OpenZeppelin的UUPSUpgradeable越来越多的新项目Beacon 代理Beacon代理是一种允许同时升级多个代理合约的模式。在这种模式中，所有代理合约都指向一个Beacon合约，而Beacon合约则指向实现合约。当需要升级时，只需要更新Beacon合约中的实现地址，所有代理合约都会自动指向新的实现。// Beacon 合约 contract UpgradeableBeacon { address private _implementation; address private _owner; function implementation() public view returns (address) { return _implementation; } function upgradeTo(address newImplementation) public { require(msg.sender == _owner, "Not authorized"); _implementation = newImplementation; } } // Beacon 代理合约 contract BeaconProxy { address private _beacon; function _implementation() internal view returns (address) { return IBeacon(_beacon).implementation(); } function _fallback() internal { _delegate(_implementation()); } }优点：可以同时升级多个代理合约降低了升级的gas成本缺点：增加了一层间接性每次调用都需要额外的gas来获取实现地址应用场景：需要部署多个相同逻辑的可升级合约实际应用：OpenZeppelin的BeaconProxy一些NFT和DeFi项目安全考虑常见漏洞存储冲突：代理合约和实现合约使用相同的存储槽，可能导致数据覆盖功能选择器冲突：代理合约和实现合约有相同的函数选择器，可能导致调用错误的函数未初始化的代理：代理合约未正确初始化，可能被攻击者利用delegatecall 和 selfdestruct：实现合约中使用这些操作可能导致代理合约被销毁或逻辑被篡改升级权限：升级权限过于集中，可能导致单点故障最佳实践使用无结构存储：避免存储冲突使用透明代理或UUPS：避免功能选择器冲突确保正确初始化：防止未初始化的代理漏洞避免在实现中使用危险操作：不要在实现合约中使用delegatecall和selfdestruct实现多签或时间锁：分散升级权限全面测试：在升级前进行全面的测试，确保新实现与旧实现兼容使用成熟的库：如OpenZeppelin的代理库未来发展新的代理模式随着区块链技术的发展，新的代理模式不断涌现：钻石模式（Diamond Pattern）：允许一个代理合约指向多个实现合约，每个实现合约负责不同的功能可组合代理：允许代理合约组合多个实现合约的功能EIP和标准化社区正在努力标准化代理模式，以提高互操作性和安全性：EIP-1822：提出了UUPS标准EIP-2535：提出了钻石模式标准总结代理模式是区块链开发中解决合约不可变性问题的关键技术。通过使用代理模式，开发者可以在保持合约地址不变的情况下升级合约逻辑，提高了区块链应用的灵活性和可维护性。然而，代理模式也带来了新的安全挑战，开发者需要了解各种代理模式的优缺点，选择适合自己项目的模式，并遵循最佳实践来避免潜在的漏洞。随着区块链技术的不断发展，代理模式也在不断演进，新的模式和标准将为开发者提供更多选择，使区块链应用更加强大和灵活。参考资料OpenZeppelin Proxy ContractsEIP-1967: Standard Proxy Storage SlotsEIP-1822: Universal Upgradeable Proxy Standard (UUPS)EIP-2535: Diamond Standard
在以太坊生态系统中，ERC-20代币是最常见的代币标准之一。它定义了代币的基本接口，包括转账、余额查询等功能。然而，随着 DeFi（去中心化金融）的快速发展，传统的ERC-20代币在某些场景下显得不够灵活。特别是在需要用户多次授权合约操作时，用户需要多次签名并支付Gas费用，这不仅增加了用户的操作成本，还降低了用户体验。为了解决这个问题，EIP-2612提出了permit功能，允许用户通过链下签名的方式授权合约操作，从而减少链上交易次数和Gas消耗。本文将详细介绍permit功能的实现原理、使用场景以及如何在智能合约中集成这一功能。一、什么是Permitpermit是ERC-20代币的一个扩展功能，它允许用户通过链下签名的方式授权第三方合约或地址使用其代币。与传统的approve方法不同，permit不需要用户发送链上交易，而是通过签名的方式在链下完成授权。合约可以通过验证签名来获取用户的授权，从而减少用户的Gas消耗。1.1 传统approve的问题在传统的ERC-20代币中，用户需要通过approve方法来授权第三方合约或地址使用其代币。例如：function approve(address spender, uint256 amount) external returns (bool);这个过程需要用户发送一笔链上交易，并支付Gas费用。如果用户需要多次授权不同的合约或地址，每次都需要发送一笔交易，这不仅增加了用户的操作成本，还降低了用户体验。1.2 permit的优势permit功能通过链下签名的方式解决了这个问题。用户只需在链下签名一次，合约可以通过验证签名来获取用户的授权。这样，用户不需要发送链上交易，从而减少了Gas消耗。二、Permit的实现原理permit功能的实现依赖于EIP-712标准，该标准定义了一种结构化的签名方式，使得链下签名可以被链上合约验证。2.1 EIP-712结构化签名EIP-712定义了一种结构化的签名方式，允许用户在签名时包含更多的上下文信息。这样，用户在签名时可以清楚地知道自己在签署什么内容，从而减少签名的风险。permit功能使用EIP-712标准来生成和验证签名。具体来说，permit的签名数据结构如下：struct Permit { address owner; address spender; uint256 value; uint256 nonce; uint256 deadline; }owner: 代币的所有者地址。spender: 被授权的地址。value: 授权的代币数量。nonce: 所有者的当前nonce值，用于防止重放攻击。deadline: 签名的过期时间。2.2 Permit的签名过程生成签名数据：用户根据Permit结构生成签名数据，并使用EIP-712标准进行结构化签名。发送签名：用户将签名发送给第三方合约。验证签名：合约通过验证签名来获取用户的授权，并更新用户的allowance。2.3 Permit 的合约实现在智能合约中，permit 功能通常通过以下方式实现：function permit( address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) external { require(block.timestamp <= deadline, "Permit: expired"); bytes32 structHash = keccak256( abi.encode( PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline ) ); bytes32 hash = _hashTypedDataV4(structHash); address signer = ECDSA.recover(hash, v, r, s); require(signer == owner, "Permit: invalid signature"); _approve(owner, spender, value); }PERMIT_TYPEHASH: 是Permit结构的类型哈希，用于EIP-712签名。nonces[owner]: 是用户的当前 nonce 值，用于防止重放攻击。_hashTypedDataV4: 是EIP-712的哈希函数，用于生成结构化哈希。ECDSA.recover: 用于从签名中恢复签名者地址。三、使用Permit3.1 permit功能在以下场景中非常有用减少Gas消耗：在DeFi应用中，用户通常需要多次授权合约操作。使用permit功能，用户只需在链下签名一次，合约可以通过验证签名来获取用户的授权，从而减少用户的Gas消耗。提高用户体验：传统的approve方法需要用户发送链上交易，而permit功能允许用户在链下签名，从而提高了用户体验。用户可以在不发送链上交易的情况下完成授权操作。防止重放攻击：permit功能使用nonce和deadline来防止重放攻击。每次签名都有一个唯一的nonce值，并且签名有一个过期时间，从而确保签名的安全性。3.2 在智能合约中集成Permit要在智能合约中集成permit功能，可以按照以下步骤进行：继承ERC-20Permit合约OpenZeppelin提供了一个ERC20Permit合约，可以直接继承并使用。ERC20Permit合约已经实现了permit功能，只需继承该合约即可。import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol"; contract MyToken is ERC20Permit { constructor() ERC20("MyToken", "MTK") ERC20Permit("MyToken") {} }使用Permit接口在合约中使用permit功能时，可以通过以下方式调用：function depositWithPermit( uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) external { token.permit(msg.sender, address(this), amount, deadline, v, r, s); token.transferFrom(msg.sender, address(this), amount); }3.3 前端用户生成Permit签名前端用户可以使用ethers库快速生成签名 const token = await ethers.getContractAt(tokenAbi, tokenAddr, signer); const nonce = await token.nonces(signer); const name = await token.name(); const version = await token.version(); const amount = ethers.MaxUint256; const deadline = Math.floor(Date.now()/1000) + 3600; const domain = { name, version, chainId: chainId, verifyingContract: token.target, }; const types = { Permit: [ { name: "owner", type: "address" }, { name: "spender", type: "address" }, { name: "value", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "deadline", type: "uint256" }, ], }; const value = { owner: signer.address, spender: router.target, value: amount, nonce, deadline, }; const signature = ethers.Signature.from(await signer.signTypedData(domain, types, value)); // signature.v, // signature.r, // signature.s,四、进阶版Permit2Permit2是Uniswap团队提出的一种新型代币授权标准，旨在解决传统approve和EIP-2612 permit的局限性。它通过引入一个全局的、可重用的授权机制，允许用户在一次链下签名中授权多个合约或操作，从而进一步减少Gas消耗并提升用户体验。Permit2的名称反映了与Permit2合约交互以实现授权转移的两种方式。尽管授权和基于签名的转移之间存在区别，但这两种交互类型都使用签名：基于授权的转移：通过签名处理代币授权，转移检查允许的金额。这是在预期多次转移时更高效的解决方案。‍基于签名的转移：直接通过签名处理代币转移。对于一次性转移更高效。4.1 传统approve和permit的局限性多次授权问题：在传统approve和permit中，用户需要为每个合约或操作单独授权。例如，如果用户想在多个DeFi平台上使用代币，每次都需要发送一笔授权交易。Gas消耗高：每次授权都需要支付Gas费用，尤其是在以太坊网络拥堵时，Gas费用会显著增加。安全性问题：传统的approve方法存在潜在的安全风险，例如授权额度过高可能导致资金被盗。4.2 Permit2的优势全局授权：Permit2允许用户在一次链下签名中授权多个合约或操作，从而减少链上交易次数。Gas优化：通过集中管理授权，Permit2显著降低了Gas消耗。安全性增强：Permit2引入了更灵活的授权机制，例如时间限制和额度限制，从而提高了安全性。4.3 Permit2的设计理念Permit2的核心设计理念是通过一个全局的、可重用的授权机制，简化代币授权流程并提高效率。具体来说，Permit2实现了以下功能：集中管理授权,Permit2引入了一个全局的授权管理器（Authorization Manager），用户可以通过一次链下签名授权多个合约或操作。授权管理器会记录用户的授权信息，并在需要时提供给合约使用。灵活的授权策略,Permit2支持多种授权策略，例如：时间限制：用户可以设置授权的有效期，过期后授权自动失效。额度限制：用户可以设置授权的最大额度，防止合约滥用授权。操作限制：用户可以限制授权的具体操作，例如只能用于转账或只能用于批准。4.4 兼容性Permit2完全兼容现有的ERC-20代币标准，并且可以与EIP-2612 permit功能共存。这意味着开发者可以逐步迁移到Permit2，而无需修改现有的代币合约。Permit2也有一些独特的缺点。首先，先决步骤迫使用户批准其代币到Permit2合约，这对用户体验和采用造成了静态摩擦。其次，攻击面较窄，直接指向Permit2合约。好消息是这些合约简洁、编写良好、经过测试和审计。4.5 工作流程用户的先决步骤。他们必须对其代币进行传统的批准，以便将其发送到Permit2合约。通常以uint256最大值进行，只需用户为其代币执行一次。一旦完成，任何与Permit2集成的 dApp只需请求用户的链下签名即可利用已授予的权限。Permit2获得用户代币批准后的操作。用户通过链下签名表达其允许特定 dApp 支出者合约移动其代币的意图。支出者充当快递员，将意图传递给 Permit2 合约，可以视为支出者与用户之间的守门人或中介。如果 Permit2 合约验证签名并明确要求正确的数据，则将使用预先批准的授权代表用户将代币转移给支出者。一旦支出者收到代币，它可以执行用户请求的必要操作。4.6 Permit2签名生成 const amount = ethers.parseUnits("10", 6); const deadline = Math.floor(Date.now()/1000) + 3600; const nonce = 0; const domain = { name: "Permit2", chainId: chainId, verifyingContract: permit2.target, }; const types = { PermitBatchTransferFrom: [ { name: "permitted", type: "TokenPermissions[]" }, { name: "spender", type: "address" }, { name: "nonce", type: "uint256" }, { name: "deadline", type: "uint256" }, ], TokenPermissions: [ { name: "token", type: "address" }, { name: "amount", type: "uint256" }, ], }; const value = { permitted: [ { token: usdt.target, amount: amount, }, ], spender: router.target, nonce: nonce, deadline: deadline, }; const signature = await signer.signTypedData(domain, types, value);五、总结permit功能通过链下签名的方式，允许用户在不发送链上交易的情况下授权第三方合约或地址使用其代币。这不仅减少了用户的 Gas消耗，还提高了用户体验。随着DeFi的快速发展，permit功能将在更多的应用场景中得到广泛应用。
通用高级的抽象的合约虚拟机设计规范账户抽象账户信息由字节码（Code）和存储（Storage）构成使用20长度字节数组类型的值来唯一标识（ID）账户以账户ID为键，以Code和Storage为值，构建Verkle树来保存所有账户的状态任何账户都可以接收输入（Input），在虚拟机里执行代码并保存新的执行结果（比如转账）如果账户的字节码为空，将使用具有默认接口功能的代码替代（这个代码可以采用硬编码的方式提供）因为所有账户都可以有字节码和存储，所以账户都可以视为合约预先部署一个代币合约，作为原生数字货币，用于矿工奖励发放和用户费用支付预先部署一个共识合约，接收区块输入，并验证、执行和存储它合约虚拟机在虚拟机中，所有账户统称为合约，合约执行代码需要消耗燃料（Gas），按照代码的复杂度来收取硬编码合约将区块链的一些核心功能转化为硬编码的合约实现，有以下优势相比字节码的方式合约调用能带来一致性的体验有效减少字节码的数量可以采用硬编码的方式实现，性能不会有太大影响从账户0x1开始分配特殊字节码购买Gas：共识合约会提供一个支付代币购买的接口，经过接口验证后内部调用此字节码为其增加Gas，仅共识合约可以调用重置合约：清空合约存储数据并使用新的字节码初始化（可以指定为空），类似以太坊合约部署，仅限重置账户自己创建合约：使用指定的字节码去初始化一个随机的合约，类似以太坊CREATE2字节码合约可以自定义代码来关闭重置功能，类似以太坊账户的合约销毁机制：不存在调用销毁字节码逻辑的合约是无法被销毁的交易执行用户指定一个目标合约和输入（符合一定规则的，一般会包含签名以及合约调用），发送到虚拟机中加载合约对应的代码开始执行，初始时虚拟机会暂借一定量的Gas（MinGas），这些Gas主要用调用购买Gas接口交易可以在耗尽MinGas之前调用共识合约提供的Gas购买接口添加更多Gas，否则交易会失败失败的交易会回滚状态，但不返还消耗来购买Gas的代币；不管成功或失败，多余的Gas会进行返还处理如果一笔交易没有购买过Gas，则不应该打包进区块中（取决于矿工逻辑）在交易结束（不管成功或失败）前都没有购买Gas的交易可以视为垃圾交易，理应直接丢弃垃圾交易为了避免无效交易的DOS攻击，需要在交易执行消耗少量Gas的时候就进行判断，如果还没有账户为这笔交易买单，则应该终止执行并将其视为垃圾交易。因为矿工激励规则，没有账户付Gas的交易矿工收益为零，所以矿工有权拒绝打包这类交易。灵活收费将Gas收费逻辑转移到虚拟机中实现，只要交易目标合约调用了共识合约的购买Gas接口即可，目标合约可以在购买Gas之前执行一些自定义代码（比如签名验证或其它一些检查），共识合约可以自定义购买Gas费的逻辑（比如某些账户免费或阶梯购买）交易安全合约有默认的接口，使用对应的私钥进行控制，用户可以更新代码实现自定义的规则，实现多签、托管、恢复等等功能，达到灵活控制账户的目的，后期还能采用升级默认的合约接口来提高安全性。重放攻击由账户自行防御，可采用类似nonce的机制或其它方法默认合约接口链合约存储区块（调用共识合约验证）查询区块参数配置查询共识合约验证一个新的区块执行其中的交易执行激励规则验证一笔交易系统合约重置账户创建账户删除账户
近年来，区块链技术的快速发展催生了众多创新应用，而Ton链作为其中的佼佼者，凭借其高效、低成本的特性，吸引了大量开发者和用户的关注。与此同时，Telegram作为全球最受欢迎的即时通讯应用之一，也在不断扩展其生态系统。当Ton链与Telegram相遇，便诞生了令人兴奋的Telegram小程序，为用户带来了全新的区块链体验。简介Ton（全称：The Open Network）是一个由多个组成部分构成的去中心化和开放的互联网平台。这些组成部分包括：TON区块链、TON DNS、TON存储和TON网站。TON区块链是TON底层基础设施的核心，由Telegram团队最初设计和社区进行开发。旨在为用户提供快速、安全且低成本的去中心化服务。Ton链的设计理念是让区块链技术更加普及，尤其是在支付、存储和通信等领域。其核心技术包括：高吞吐量：Ton链能够处理数百万笔交易，远超传统区块链。低交易费用：用户可以在几乎零成本的情况下完成交易。智能合约支持：开发者可以在Ton链上构建去中心化应用（DApps）。Telegram不仅仅是一个聊天工具，它还是一个开放的平台，允许开发者通过其API和Bot功能创建各种小程序。这些小程序可以集成到聊天中，为用户提供丰富的功能，例如支付、游戏、新闻推送等。随着Ton链的推出，Telegram小程序的功能得到了进一步扩展，尤其是在区块链领域。Telegram小程序（TMA）是在Telegram中运行的Web应用，使用Web技术构建——HTML、CSS和JavaScript。在Telegram上构建基于Ton链的去中心化的小程序应用，即可拥有潜在的大量Telegram用户基础。核心步骤在Telegram上面创建机器人，并在机器人下面创建小程序，创建过程中按照提示操作即可使用Web技术和Telegram提供的SDK编写小程序前端，构建且部署，得到一个链接，过程和开发部署一般Web网站一样将创建的小程序和部署的前端链接绑定起来，就是修改小程序的Web App URL为上一步部署好的地址然后在Telegram客户端里面打开小程序，可在小程序中连接Ton链，开发去中心化类别的应用打开BotFather，并使用/newbot命令，再使用/newapp命令，官方教程Ton钱包Ton Connect协议有许多的Ton链的钱包提供支持，只要钱包支持这个协议，使用此协议的小程序就可以连接到这个Ton链钱包。连接钱包后，能获取到钱包的信息以及账户信息，还可以签名并发送交易到Ton链上，实现与区块链交互，构建去中心化类别的小程序应用。小程序集成步骤（其它Web前端也是如此）如下：安装SDK：yarn add @tonconnect/ui初始化：准备一个manifest.json文件，配置到项目的根目录，例如https://myapp.com/tonconnect-manifest.json在应用程序中导入SDK，创建@tonconnect/ui的连接器实体对象接下来就可以使用此对象提供的接口发送交易，进行Ton转账等区块链操作官方教程STON.fiSTON.fi是一个基于TON区块链建立的去中心化自动做市商 (AMM)，提供几乎零费用、低滑点、极其简单的界面以及支持众多的TON钱包连接。并且提供了SDK，前端可轻松集成后进行代币的兑换。它提供了三种兑换方式（jetton是Ton链的代币，类似于以太坊的ERC20）：TON => jettonjetton => jettonjetton => Ton下面是使用Ton币购买jetton代币的示例，更多细节参考官方文档import { TonClient, toNano } from "@ton/ton"; import { DEX, pTON } from "@ston-fi/sdk"; const client = new TonClient({ endpoint: "https://toncenter.com/api/v2/jsonRPC", }); const router = client.open(new DEX.v1.Router()); // swap 1 TON to STON but not less than 1 nano STON const txParams = await router.getSwapTonToJettonTxParams({ userWalletAddress: "", // ! replace with your address proxyTon: new pTON.v1(), offerAmount: toNano("1"), askJettonAddress: "EQA2kCVNwVsil2EM2mB0SkXytxCqQjS4mttjDpnXmwG9T6bO", // STON minAskAmount: "1", queryId: 12345, });除了智能合约和前端SDK，它还提供REST API来简化与STON.fi协议的交互。其API整合来自STON.fi合约的数据，并汇总有关池、jetton以及整个STON.fi协议等的数据。比如下面的接口可进行资产查询：https://api.ston.fi/v1/assets/search?search_string=Ton更多接口查看swagger文档小程序模板可克隆下面模板，快速进行小程序的开发React.js https://github.com/Telegram-Mini-Apps/reactjs-templateNext.js https://github.com/Telegram-Mini-Apps/nextjs-templateSolid.js https://github.com/Telegram-Mini-Apps/solidjs-js-template参考文档https://docs.ton.org/mandarin/develop/dapps/telegram-appshttps://docs.telegram-mini-apps.com/zh/platform/creating-new-apphttps://docs.ston.fi/docs
基于hardhat框架进行合约开发，使用OpenZeppelin库实现可升级的智能合约搭建开发环境操作系统：Linuxnode：23.1.0yarn：1.22.22hardhat：2.22.171、创建空白项目mkdir demo && cd demo yarn init -y可更改yarn的源（默认的太慢还可能导致安装包失败）：yarn config set registry https://registry.npmmirror.com2、集成hardhat添加包到项目中yarn add -D hardhat初始化环境yarn hardhat选择创建JavaScript项目，并按照提示创建相关文件和安装依赖包888 888 888 888 888 888 888 888 888 888 888 888 888 888 888 8888888888 8888b. 888d888 .d88888 88888b. 8888b. 888888 888 888 "88b 888P" d88" 888 888 "88b "88b 888 888 888 .d888888 888 888 888 888 888 .d888888 888 888 888 888 888 888 Y88b 888 888 888 888 888 Y88b. 888 888 "Y888888 888 "Y88888 888 888 "Y888888 "Y888 Welcome to Hardhat v2.22.17 ✔ What do you want to do? · Create a JavaScript project ✔ Hardhat project root: · /home/swwx/demo ✔ Do you want to add a .gitignore? (Y/n) · y ✔ Do you want to install this sample project's dependencies with yarn (@nomicfoundation/hardhat-network-helpers @nomicfoundation/hardhat-verify chai hardhat-gas-reporter solidity-coverage @nomicfoundation/hardhat-ignition @nomicfoundation/hardhat-toolbox @nomicfoundation/hardhat-chai-matchers @nomicfoundation/hardhat-ethers ethers @typechain/hardhat typechain @typechain/ethers-v6 @nomicfoundation/hardhat-ignition-ethers)? (Y/n) · y初始化时自动配置了一个示例合约，可以运行以下命令查看效果yarn hardhat compile：编译智能合约yarn hardhat test：执行合约测试yarn hardhat node：启动一个虚拟的本地测试节点yarn hardhat help：查看帮助信息3、安装openzeppelin升级插件和合约库yarn add -D @openzeppelin/hardhat-upgrades yarn add @openzeppelin/contracts @openzeppelin/contracts-upgradeable修改hardhat配置文件hardhat.config.js，导入插件require("@nomicfoundation/hardhat-toolbox"); require("@openzeppelin/hardhat-upgrades"); /** @type import('hardhat/config').HardhatUserConfig */ module.exports = { solidity: "0.8.28", };开发升级合约部署到EVM的合约代码是不能修改的，一般可以使用代理合约的方式实现合约升级，代理合约保存数据，逻辑合约保存可执行代码，并使用delegatecall字节码连接在一起。一般来说，升级代码实现有两种方式，一是在将升级管理代码放在代理合约中，但这样会增加代理合约的大小，增加部署代理的成本，二是将升级代码放到实现合约中，由代理合约使用delegatecall字节码进行调用，这样会增加逻辑合约的大小，但是可以在将来升级的时候禁用升级接口，实现不再更新合约的功能。通常，代理模式使用单个实现合约和单个代理合约。然而，多个代理也可以使用相同的实现，这种代理叫做信标代理。信标代理将实现地址存储到信标合约中，调用接口时代理会去信标合约获取最新的实现地址，由于多个代理使用一个信标，在更新信标合约中的实现地址时，这些代理间接获得更新，相当于“批量升级合约“。初始的Hardhat项目默认有一个Lock合约及其测试，可以参考修改或者删除1、Transparent（透明代理）（代理合约管理升级）编写可升级的智能合约，创建代币合约MyTokenV1.sol// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.28; import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; contract MyTokenV1 is ERC20Upgradeable { function initialize() public initializer { __ERC20_init("MyTokenV1", "MTV1"); } }创建部署脚本scripts/deployV1.jsconst { ethers, upgrades } = require("hardhat"); async function main() { const MyTokenV1 = await ethers.getContractFactory("MyTokenV1"); const myTokenV1 = await upgrades.deployProxy(MyTokenV1); await myTokenV1.waitForDeployment(); console.log("MyToken V1 deployed to:", await myToken.getAddress()); } main();启动本地虚拟节点并部署合约yarn hardhat node # 打开新的终端后执行部署命令 yarn hardhat run scripts/deployV1.js --network localhost增加V2版本的合约MyTokenV2.sol// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.28; import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; contract MyTokenV2 is ERC20Upgradeable { function initialize() external initializer { __ERC20_init("MyTokenV2", "MTV2"); } function owner() public pure returns (address) { return address(0); } function name() public pure override returns (string memory) { return "MyTokenV2"; } function symbol() public pure override returns (string memory) { return "MTV2"; } }创建升级脚本scripts/upgradeV2.jsconst { ethers, upgrades } = require("hardhat"); async function main() { // 这里填入之前部署的代理合约地址（上一步部署时控制台输出的） const V1_ADDRESS = "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512"; const MyTokenV2 = await ethers.getContractFactory("MyTokenV2"); await upgrades.upgradeProxy(V1_ADDRESS, MyTokenV2); console.log("MyToken upgraded to V2"); } main();执行升级合约脚本yarn hardhat run scripts/upgradeV2.js --network localhost验证合约升级的效果# 打开hardhat本地节点控制台 yarn hardhat console --network localhost # 在控制台输入以下命令进行确认 > const MyToken = await ethers.getContractFactory("MyTokenV2"); undefined > const myToken = await MyToken.attach("0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512"); undefined > await myToken.name(); 'MyTokenV2'可以看到输出的代币名称为MyTokenV2，表示合约升级成功（V1版本的代币名称为MyTokenV1）2、UUPS（通用可升级代理标准）（实现合约管理升级）合约需要继承UUPSUpgradeable合约，部署和升级的脚本和透明代理的一样无需改动，openzeppelin升级工具会自动选择合适的代理部署。编写可升级的智能合约，创建代币合约MyTokenV1.sol// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.28; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; contract MyTokenV1 is Initializable, OwnableUpgradeable, UUPSUpgradeable, ERC20Upgradeable { /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } function initialize(address initialOwner) external initializer { __Ownable_init(initialOwner); __UUPSUpgradeable_init(); __ERC20_init("MyTokenV1", "MTV1"); } function _authorizeUpgrade( address newImplementation ) internal override onlyOwner {} }增加V2版本的合约MyTokenV2.sol// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.28; import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; contract MyTokenV2 is OwnableUpgradeable, UUPSUpgradeable, ERC20Upgradeable { function initialize(address initialOwner) external initializer { __Ownable_init(initialOwner); __ERC20_init("MyTokenV2", "MTV2"); } function _authorizeUpgrade(address) internal pure override { assert(false); } function owner() public pure override returns (address) { return address(0); } function name() public pure override returns (string memory) { return "MyTokenV2"; } function symbol() public pure override returns (string memory) { return "MTV2"; } }验证合约升级的效果# 打开hardhat本地节点控制台 yarn hardhat console --network localhost # 在控制台输入以下命令进行确认 > const MyToken = await ethers.getContractFactory("MyTokenV2"); undefined > const myToken = await MyToken.attach("0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512"); undefined > await myToken.name(); 'MyTokenV2' > await myToken.owner(); '0x0000000000000000000000000000000000000000'可以看到输出的代币名称为MyTokenV2，表示合约升级成功（V1版本的代币名称为MyTokenV1）3、 Beacon（信标代理）（批量升级合约）编写可升级的智能合约，创建代币合约MyTokenV1.sol// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.28; import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; contract MyTokenV1 is ERC20Upgradeable { function initialize() public initializer { __ERC20_init("MyTokenV1", "MTV1"); } }创建部署脚本scripts/deployV1.jsconst { ethers, upgrades } = require("hardhat"); async function main() { const MyTokenV1 = await ethers.getContractFactory("MyTokenV1"); const beacon = await upgrades.deployBeacon(MyTokenV1); await beacon.waitForDeployment(); console.log("Beacon deployed to:", await beacon.getAddress()); const myTokenV1 = await upgrades.deployBeaconProxy(beacon, MyTokenV1); await myTokenV1.waitForDeployment(); console.log("MyToken V1 deployed to:", await myTokenV1.getAddress()); } main();启动本地虚拟节点并部署合约yarn hardhat node # 打开新的终端后执行部署命令 yarn hardhat run scripts/deployV1.js --network localhost增加V2版本的合约MyTokenV2.sol// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.28; import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; contract MyTokenV2 is ERC20Upgradeable { function initialize() external initializer { __ERC20_init("MyTokenV2", "MTV2"); } function owner() public pure returns (address) { return address(0); } function name() public pure override returns (string memory) { return "MyTokenV2"; } function symbol() public pure override returns (string memory) { return "MTV2"; } }创建升级脚本scripts/upgradeV2.jsconst { ethers, upgrades } = require("hardhat"); async function main() { // 这里填入之前部署的信标合约地址（上一步部署时控制台输出的） const BEACON_ADDRESS = "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0"; const MyTokenV2 = await ethers.getContractFactory("MyTokenV2"); await upgrades.upgradeBeacon(BEACON_ADDRESS, MyTokenV2); console.log("Beacon upgraded"); } main();执行升级合约脚本yarn hardhat run scripts/upgradeV2.js --network localhost验证合约升级的效果# 打开hardhat本地节点控制台 yarn hardhat console --network localhost # 在控制台输入以下命令进行确认 > const MyToken = await ethers.getContractFactory("MyTokenV2"); undefined > const myToken = await MyToken.attach("0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9"); undefined > await myToken.name(); 'MyTokenV2'可以看到输出的代币名称为MyTokenV2，表示合约升级成功（V1版本的代币名称为MyTokenV1）参考文档openzeppelin合约文档：https://docs.openzeppelin.com/contracts升级插件文档：https://docs.openzeppelin.com/upgrades-plugins代理（升级）合约：https://docs.openzeppelin.com/contracts/api/proxy#transparent-vs-uups完整的开发流程指南：https://docs.openzeppelin.com/learn升级合约总结：https://blog.openzeppelin.com/the-state-of-smart-contract-upgrades