搜索
NFT元宇宙Web3
近期热门

智能合约的可升级性

Founder

原文作者:foobar

在不可篡改的区块链上更新应用程序,就像在飞行中更换飞机引擎一样。

流行的NFT项目AkuDreams意外地将3400万美元的全部筹款永远锁定在一个智能合约中。我在这里写了一个推特线程,详细介绍了这个漏洞。

智能合约的可升级性

有些人想知道,为什么不直接修复代码?漏洞是软件开发不可避免的一部分;构建管道和版本升级是每个技术栈的关键部分。

智能合约的可升级性

答案很简单–智能合约字节码一旦部署就不可改变。但这掩盖了一个复杂的系统,开发人员已经创建了一个复杂的系统,在不可变的基础层上编写可升级的应用程序。让我们更深入地了解一下。

智能合约如何运作

这里我们讨论的是运行在以太坊虚拟机(EVM)上的Solidity智能合约,以太坊、Binance智能链、Avalanche、Polygon、Fantom等链所使用的编程环境。

智能合约的可升级性

EVM的一个有趣的问题是,用户账户和智能合约占据相同的40个十六进制字符的地址空间。不同的是,外部拥有的账户(EOAs)是由私钥操作的,而智能合约不能自己发送交易。合约部署的字节码提供了EOAs可以调用的功能,但合约不能自行触发。

区块链上的 “cronjob “等价物被称为keeper job,你向Chainlink/Keep3r/其他人运行的机器人网络付费,以定期调用upkeep功能,如清算、重新平衡或自动收获你的智能合约。

智能合约是存储在区块链上的可执行字节码。一旦代码被部署到智能合约的地址,代码就永远不能被改变(除了自毁,这是一个全面的删除)。那么,如果代码不能被改变,我们如何处理可升级性?

智能合约的可升级性

我将探讨这里使用的三种关键方法,从 “最不可变 “到 “最灵活”。

1. 存储参数

每个合约都有自己的存储范围,只有它能触及。最常见的原始类型是整数、地址、映射和数组。这些存储变量不是不可改变的,函数逻辑可以随时改变它们。

因此,最简单的升级方式是使用一个治理锁定的方法来更新某些经济参数。如果你正在运行一个StakingPools合约,你可以更新代币给stakers的奖励率。如果你正在运行一个LendingPools合约,你可以更新存款或借款利率。这不会改变字节码,因为它只是更新存储槽中持有的价值。

智能合约的可升级性

2. 合约指针

有时你可能想检修一个合约的逻辑,而不仅仅是一个参数。所以你可能会有一个主调度合约,它持有实际合约的地址并在那里进行调用。

一个很好的例子是Aave V3的代码库。主入口调用一个已知的地址提供者合约,该合约为系统的移动部件提供地址指针。治理者可以在新的地址上用新的字节码部署新的合约,然后将地址提供者的存储变量从$OLD_ADDRESS更新到$NEW_ADDRESS。

智能合约的可升级性

一个经过轻度编辑的质押池自动合成包装器。治理可以从一个质押池迁移到另一个,当且仅当它们符合相同的函数接口。

3.代理人

智能合约有不可变的字节码和可变的存储环境。通常情况下,无论字节码是进行外部调用还是修改自己的存储,这些都是无缝衔接的。

但是EVM有一个特殊的操作码,叫做delegatecall,它从一个不同的、外部的合约中获取字节码,并在原始合约的存储上下文中执行该字节码。这在某种程度上相当于从互联网上下载一个脚本并在你的家庭电脑上运行,所以这应该只在已知的可信任的外部合约上使用,要非常谨慎。

智能合约的可升级性

让我们以ERC20代币为例。字节码是几个函数,指定谁有资格铸造,是否有转让税,是否可以暂停,等等。存储上下文包含数据,如持有人地址与持有人余额的映射。使用delegatecall意味着ERC20负责保持其存储环境/持有人余额,但它将功能逻辑/造币规则外包给外部智能合约。而且你可以切换出你委托给哪个智能合约的功能逻辑。

这很有用,因为它可以让你修补逻辑错误,而不会丢失任何重要的存储环境。这也是相当有表现力的。治理者可以部署他们想要的任何逻辑,包括恶意行为,如无限铸币或烧毁特定持有人的代币,或意外地部署一个合约升级,巨大的权力伴随着巨大的责任。

高级:开发细节

这是一个额外的技术部分。如果你不是一个活跃的 Solidity 开发者,请跳过。

包含存储上下文的合约被称为 “代理合约”,而包含字节码实现的合约被称为 “逻辑合约”。第三方用户将对代理合同进行调用,然后它将在引擎盖下获取逻辑合同的字节码,并在代理的存储上下文中执行。

如果编写可升级的逻辑合约,这里有一些Solidity的细微差别需要正确掌握。

所有的逻辑合约必须有空的构造器,因为构造器在部署时在逻辑合约的存储上下文中被原子化地执行。通常在构造函数中进行的设置应该在部署后被转移到代理的存储上下文中单独调用的初始化函数。

EVM以一种非常特殊的方式布置存储,所以必须注意不要重新组织变量声明的顺序。最好的做法是总是以相同的顺序进行存储声明,如果需要新的存储,那么应该放在所有以前的声明之后。

还有一些需要担心的函数冲突–代理合同有几个用于升级其逻辑合同指针的最小函数,如 owner() 和 upgradeTo()。显然,只有代理所有者才能升级逻辑合约。但是如果逻辑合约也有一个所有者()方法,它应该优先考虑,会发生什么?

目前的最佳做法是根据调用的地址产生不同的行为。如果调用地址是代理所有者,那么它将永远不会委托调用逻辑合约。如果调用的合约不是代理所有者,它将是委托调用。

这就产生了另一个边缘故障,代理所有者可能希望与逻辑互动,所以为了解决这个问题,部署了一个中间的ProxyAdmin合约,所有对代理的调用都通过这个合约进行。这使得代理所有者可以通过ProxyAdmin来升级逻辑合约,但也可以直接通过代理合约来与逻辑互动。

智能合约的可升级性

代理类型

虽然在所有的代理类型中,存储委托调用外部字节码的一般想法是相同的,但已经提出了几种不同的实现方式,我在这里简单地介绍一下它们。

EIP-1967标准指定了一个特定的存储槽,0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc,保证不会被编译器分配。这有助于像Etherscan这样的探索器在人们查看代理合约时解码并指向逻辑合约方法。

通用可升级代理(UUPS)

一个版本,其升级逻辑被存储在实施合约中。这意味着它可以在以后被删除,或者在忘记包含升级功能时被意外冻结。然而,凭借更集中的代码,与透明代理相比,它减少了部署和互动成本。

透明代理 (Writeup)

一个版本,升级逻辑被放置在代理本身。这使得部署更加昂贵,但在逻辑合约升级方面,减少了自相残杀的机会。

信标代理

一个版本让你在一次调用中把多个代理升级到一个新的执行地址。当你有一个给定的存储上下文的克隆或副本时很有用。

你可以在不可变的底层之上建立可变性,但你不能做相反的事情。USDC可以建立在Ethereum上,但Ethereum不能建立在SWIFT上。这是与托管自己的服务器端代码完全不同的范式,可以随心所欲地改变,但却是建立金融原语的更强大的基础。

编辑于 2022-05-25 01:35
「 真诚赞赏,手留余香 」
赞赏

发表评论已发布0

手机APP 意见反馈 返回顶部 返回底部