草庐IT

世界杯竞猜项目Dapp-第五章(合约升级)

”PANDA 2023-05-26 原文

目前主流有三种合约升级方法

  • transparent 方式;(通用,业务逻辑和代理逻辑解耦合,比较贵)
  • uups 方式;(代理逻辑集成到了业务逻辑,通过继承来实现,便宜)
  • beacon 方式;(更加高级,一个信号,升级多个合约)

本次采用 transparent 方式,具体实现思路即,引入一个代理合约 Proxy(蓝色),用户仅与这个代理合约进行交互,由代理合约去与业务合约进行交互,因此在业务合约发生变化(升级)的时候,用户无感,并且历史数据也能够保留下来,如下图所示:

既然业务合约可以随意切换,那用户数据就只能存储在代理合约中了,在实际进行业务处理时,数据读写都是从代理合约来的,即数据与逻辑分离,其实现的核心便是 delegatecall 关键字。在此之前,先对 solidity 提供的三个合约调用方法:call、staticdall、delegatecall 进行对比说明

  • call:主要是进行常规的合约调用,比如进行合约向普通EOA进行转账时,语法为:to.call{value: value}(“”),此时目标合约中的msg.sender是调用者,即Caller Contract;
  • staticcall:与call类似,但是它不会修改合约状态,单纯调用计算而已,原理同上;(不常用)
  • delegatecall:这个是专门为代理合约 Proxy 准备的,作用是帮助用户 User 来调用 Target Contract 合约

delegatecall 特点:
1、从 Target Contract 角度来看:msg.sender 是 user,而不是 Proxy,即 Proxy 对 user 的请求进行了透传;
2、在 Target Contract 被调用时,使用的是 Proxy 的上下文,即执行合约带来的状态变化会存在 Proxy 中,而不是 Target Contract 之中
(注:由于 Transparent 模式升级时,implementation 和 proxy 不用相互关心彼此的 storage 数据,因此这种模式被称为:unstructed storage)

存储冲突问题

在 solidity 中,状态变量存储在 slot 中,slot 可以理解为 key-value 存储空间,evm 为每个合约代码提供了最多 2^256个 slot,每个 slot 可以最多存储 32 字节数据。状态变量一般是从 slot 0 开始进行存储的,在使用 delegatecall 的时候,由于需要在 Proxy 的 slot 中存储目标合约中指定的数据结构,此时如果 proxy 的 storage 布局与目标合约的 storage 布局不相同,那么就会出现存储冲突(Storage collisioin)的问题。

slot 0 分别存储的是逻辑合约地址 _imp 和管理员地址 _owner,出现存储冲突。

解决存储冲突问题

在代理合约 Proxy 中,一共需要指定两个状态变量:

  • 逻辑合约地址 implementation,用来指明被代理的合约;
  • Admin,代理合约的管理员,有权限进行合约升级;

因此,如果不进行特殊处理,则一定会出现存储 slot 冲突,我们可以将 Proxy 中的默认 slot 留出来,不要占用,而是在代理合约使用指定的 slot 来存储逻辑合约 _imp 和 admin 地址,这个解决方案也叫 EIP-1967

# implementation slot 生成规则:
bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1), 

# admin slot 生成规则:
bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1)),

升级规则

1 在升级的合约中,如果有新变量的添加,那么新的状态变量只能在原始合约状态末尾依次往后添加,否则也会导致状态变量布局不一致,出现存储冲突;
2 如果一个合约定义为可升级的,那么这个合约不能有构造函数,需要使用initialize 函数来代替初始化工作。因为我们需要将部署时的数据存储在 Proxy 合约中,如果提供了构造函数,这些数据就会错误地写在逻辑合约中。

WorldCup 升级改造

首先在 node_modules 目录下安装合约升级地标准库,以使用初始化函数

npm i @openzeppelin/contracts-upgradeable

创建 WorldCupV1.sol,在原有 WorldCup 基础上修改代码

    //1. 导入标准包
    import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

    //2. 继承
    contract WorldCupV1 is Initializable {  
      //3. 将构造函数替换为初始化函数 constructor(uint256 _deadline) 
      function initialize(uint256 _deadline) public initializer {
          admin = msg.sender;
          require(
              _deadline > block.timestamp,
              "WorldCupLottery: invalid deadline!"
          );
          deadline = _deadline;
      }   
    }

再创建升级后的合约 WorldCupV2.sol,修改如下:

event ChangeDeadline(uint256 _prev, uint256 _curr);
uint256 changeCount

// 1. 增加函数,支持修改deadline
function changeDeadline(uint256 _newDeadline) external {
    require(_newDeadline > block.timestamp, "invalid timestamp!");

    // 2.增加新事件
    emit ChangeDeadline(deadline, _newDeadline); 

    // 4.状态变量
    changeCount++;  
    deadline = _newDeadline;
}

安装升级插件,在配置文件中导入

$ npm install --save-dev @openzeppelin/hardhat-upgrades

// hardhat.config.js
require('@openzeppelin/hardhat-upgrades');

编写升级脚本,创建 scripts/deployAndUpgrade.ts:

const { ethers, upgrades } = require("hardhat");

async function main() {
  const TWO_WEEKS_IN_SECS = 14 * 24 * 60 * 60;
  const timestamp = Math.floor(Date.now() / 1000)
  const deadline = timestamp + TWO_WEEKS_IN_SECS;
  console.log('deadline:', deadline)

  // Deploying
  const WorldCupv1 = await ethers.getContractFactory("WorldCupV1");
  const instance = await upgrades.deployProxy(WorldCupv1, [deadline]);
  await instance.deployed();
  console.log("WorldCupV1 address:", instance.address);
  console.log("deadline1:", await instance.deadline())

  console.log('ready to upgrade to V2...');

  // Upgrading
  const WorldCupV2 = await ethers.getContractFactory("WorldCupV2");
  const upgraded = await upgrades.upgradeProxy(instance.address, WorldCupV2);
  console.log("WorldCupV2 address:", upgraded.address);

  await upgraded.changeDeadline(deadline + 100)
  console.log("deadline2:", await upgraded.deadline())
}

main();

部署升级合约

npx hardhat run scripts/upgrade/deployAndUpgrade.ts --network goerli


我们还没 verify 业务合约 WorldCupV1 和 WorldCupV2,然后与当前的代理合约 Proxy 关联起来,我们通过 internal Txns 可以找到 WorldCupV2 的合约地址:

都对其进行 verify,并查看数据,发现都是空的

关联合约

找到代理合约-> More Options -> Is this a proxy? -> Verify -> Save

然后再回到当前页面刷新后,页面上多了两个按钮:Read as Proxy 和 Write as Proxy 如图:

我们最终暴露给用户的地址就是这个代理合约,用户的所有操作都相当于在读写着两个新方法,这两个方法会被 Proxy 传递到逻辑合约中,并把执行结果返回到代理合约中

TransparentProxy 合约关系

通过hardhat-upgrade包执行部署后,一共会自动部署三个合约:

  • 代理合约:TransparentUpgradebleProxy
  • 代理合约的管理员合约:proxyAdmin
  • 业务合约:implementation

当我们使用脚本执行合约升级的时候,此时内部交互为:

  • 自动部署 WorldCupV2 合约
  • 调用 ProxyAdmin 的 upgrade 方法,进行升级

有关世界杯竞猜项目Dapp-第五章(合约升级)的更多相关文章

  1. ruby - 如何在 buildr 项目中使用 Ruby 代码? - 2

    如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby​​

  2. ruby - 通过 rvm 升级 ruby​​gems 的问题 - 2

    尝试通过RVM将RubyGems升级到版本1.8.10并出现此错误:$rvmrubygemslatestRemovingoldRubygemsfiles...Installingrubygems-1.8.10forruby-1.9.2-p180...ERROR:Errorrunning'GEM_PATH="/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/ruby-1.9.2-p180@global:/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/rub

  3. ruby-on-rails - 项目升级后 Pow 不会更改 ruby​​ 版本 - 2

    我在我的Rails项目中使用Pow和powifygem。现在我尝试升级我的ruby​​版本(从1.9.3到2.0.0,我使用RVM)当我切换ruby​​版本、安装所有gem依赖项时,我通过运行railss并访问localhost:3000确保该应用程序正常运行以前,我通过使用pow访问http://my_app.dev来浏览我的应用程序。升级后,由于错误Bundler::RubyVersionMismatch:YourRubyversionis1.9.3,butyourGemfilespecified2.0.0,此url不起作用我尝试过的:重新创建pow应用程序重启pow服务器更新战俘

  4. ruby - 如何在 Lion 上安装 Xcode 4.6,需要用 RVM 升级 ruby - 2

    我实际上是在尝试使用RVM在我的OSX10.7.5上更新ruby,并在输入以下命令后:rvminstallruby我得到了以下回复:Searchingforbinaryrubies,thismighttakesometime.Checkingrequirementsforosx.Installingrequirementsforosx.Updatingsystem.......Errorrunning'requirements_osx_brew_update_systemruby-2.0.0-p247',pleaseread/Users/username/.rvm/log/138121

  5. ruby-on-rails - 新 Rails 项目 : 'bundle install' can't install rails in gemfile - 2

    我已经像这样安装了一个新的Rails项目:$railsnewsite它执行并到达:bundleinstall但是当它似乎尝试安装依赖项时我得到了这个错误Gem::Ext::BuildError:ERROR:Failedtobuildgemnativeextension./System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/rubyextconf.rbcheckingforlibkern/OSAtomic.h...yescreatingMakefilemake"DESTDIR="cleanmake"DESTDIR="

  6. Ruby 从大范围中获取第 n 个项目 - 2

    假设我有这个范围:("aaaaa".."zzzzz")如何在不事先/每次生成整个项目的情况下从范围中获取第N个项目? 最佳答案 一种快速简便的方法:("aaaaa".."zzzzz").first(42).last#==>"aaabp"如果出于某种原因你不得不一遍又一遍地这样做,或者如果你需要避免为前N个元素构建中间数组,你可以这样写:moduleEnumerabledefskip(n)returnto_enum:skip,nunlessblock_given?each_with_indexdo|item,index|yieldit

  7. ruby - 在不使用 RVM 的情况下在 Mac 上卸载和升级 Ruby - 2

    我最近决定从我的系统中卸载RVM。在thispage提出的一些论点说服我:实际上,我的决定是,我根本不想担心Ruby的多个版本。我只想使用1.9.2-p290版本而不用担心其他任何事情。但是,当我在我的Mac上运行ruby--version时,它告诉我我的版本是1.8.7。我四处寻找如何简单地从我的Mac上卸载这个Ruby,但奇怪的是我没有找到任何东西。似乎唯一想卸载Ruby的人运行linux,而使用Mac的每个人都推荐RVM。如何从我的Mac上卸载Ruby1.8.7?我想升级到1.9.2-p290版本,并且我希望我的系统上只有一个版本。 最佳答案

  8. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  9. ruby - 我正在学习编程并选择了 Ruby。我应该升级到 Ruby 1.9 吗? - 2

    我完全不是程序员,正在学习使用Ruby和Rails框架进行编程。我目前正在使用Ruby1.8.7和Rails3.0.3,但我想知道我是否应该升级到Ruby1.9,因为我真的没有任何升级的“遗留”成本。缺点是什么?我是否会遇到与普通gem的兼容性问题,或者甚至其他我不太了解甚至无法预料的问题? 最佳答案 你应该升级。不要坚持从1.8.7开始。如果您发现不支持1.9.2的gem,请避免使用它们(因为它们很可能不被维护)。如果您对gem是否兼容1.9.2有任何疑问,您可以在以下位置查看:http://www.railsplugins.or

  10. ruby - 如何在 Ruby 字符串中插入项目符号字符? - 2

    我正在尝试创建一个带有项目符号字符的Ruby1.9.3字符串。str="•"+"helloworld"但是,当我输入它时,我收到有关非ASCII字符的语法错误。我该怎么做? 最佳答案 你可以把Unicode字符放在那里。str="\u2022"+"helloworld" 关于ruby-如何在Ruby字符串中插入项目符号字符?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/1195

随机推荐