深入了解智能合约的最小代理“EIP-1167”
概述
在一开始,代理合约很难去掌握,在这篇文章中,我们将分析最小代理标准或“EIP-1167”,并创建一个代码示例。
在开始之前,不要混淆可升级代理和最小代理,这是非常重要的,它们是完全不同的。在这篇文章中,我们只讨论最小代理。
最小代理标准早在2018年就正式发布了,这个标准的主要思想是尽可能便宜地部署基础合约的副本。让我进一步展开:
最好的例子是一个多重签名钱包。让我们假设用户创建了一个非常简单的多重签名钱包,它可以接收资金、发送资金和设置n个所有者的数量。当然,为了发送资金,用户需要达到一定的法定人数(n≥m)。一旦智能合约准备好了,有两种主要的方法可以让它准备好投入生产。第一个是部署合约,使所有用户直接交互,这意味着所有的资金将存储在该合约,为了跟踪谁拥有什么,需要创建一个映射(地址 => uint) 公共余额+ 一些修饰符。这种方法的问题在于,用户将所有内容集中在一个地方,最重要的是,用户打开了额外攻击向量的可能性。换句话说,用户使合约更加复杂了。我们不想这样做,我们想把安全作为我们的首要任务。想到的第二种方法是让用户部署合约,为了做到这一点,用户需要编译合约,将字节码放在前端,并让用户部署合约。这种方法的问题是效率非常低,而且gas价格昂贵。想象一下,如果合约变得太大太复杂,部署成本将非常高,再加上我们正在用大量存储轰炸链。解决这个问题的方法是实现最小代理标准。
最小代理所做的是创建一个廉价的克隆(我们称它为廉价,因为它的部署成本非常低),它具有与实现合约完全相同的逻辑,但具有自己的存储状态。这是通过低级别的委托调用实现的。
为了实施这个标准,我们需要:
-
执行合约:有时被称为基础合约、核心合约、主合约等。重要的是,执行合约是所有逻辑所在的地方。
-
代理工厂或克隆工厂:顾名思义,克隆工厂合约将是我们的工厂。这意味着用户将调用工厂的一个函数,而工厂将克隆一份实施合同的精确副本,但拥有自己的存储空间。这意味着每个克隆都有相同的逻辑,但存储状态独立。
-
代理:如前所述,代理合约是实施合约的克隆,但具有独特的存储。
现在我们有了一个大致的了解,让我们创建一个示例来巩固我们的知识。我们将在Remix中做这个例子,使它更简单。
合约将会非常简单,这里的目的是理解标准。
为此我们需要以下合约:
-
实现:这是我们的逻辑所在的地方,我们将其称为Implementation.sol。
-
CloneFactory:这将是我们的工厂,我们将有一个clone() 函数,用户将触发该函数,工厂将输出代理的地址。工厂的名称将是CloneFactory.sol。
-
代理:与代理无关,代理将是CloneFactory.sol 中的clone() 函数的输出。可以有尽可能多的不同代理,这就是整个目的,以创建许多Implementation.sol的克隆。
这是它看起来的样子:
需要记住的一个非常重要的方面是,克隆不知道构造函数,因此我们使用initialize()函数“替换构造函数”,而不是使用构造函数来分配重要变量。我们只需要确保initialize()函数只被调用一次,这样人们就不能篡改合约,类似于构造函数的工作原理。为了做到这一点,我们通常使用openZeppelin的Initializabl。对于本例,我们不打算使用任何第三方合约,只是为了更清楚地说明。
让我们从Implementation.sol开始。合约唯一要做的就是拥有一个带有setter函数和修饰符的uint公共变量,限制只有所有者才能更改变量的访问权限。
让我们来分析一下:
uint public x→我们将在setter函数中更改的无符号整数(默认为0)。
bool public isBase→这个布尔值将确保实现合约永远不会被初始化。如果在构造函数中看到,我们将:isBase设置为true,并且initialize()函数的第一个require语句是require(isBase ==false)。这保证了实现合约只用于逻辑,没有人可以篡改。请记住,代理或克隆合约不知道构造函数,因此isBase将被设置为其默认值false。
address public owner→合约所有者(外部拥有的账户)。所有者默认为address(0)。在Solidity中,如果不分配地址类型,则默认值为address(0)。
modifier onlyOwner()→希望用户不需要解释这个,但基本上这是说只有所有者可以调用这个函数。
initialize(address _owner)→一旦创建代理克隆,需要立即调用initialize函数。这就像我们的构造函数,意味着如果之前有人调用这个函数,它将控制合约。如我们所见,它有一个参数(address _owner)。该参数将在CloneFactory 中提供。这里有两个重要的考虑:
-
用户需要确保initialize函数只被调用一次。我们这样做,是通过检查所有者是否是地址(0)。一旦分配了所有者,并且我们试图再次调用该函数,交易将恢复。强烈建议在Initializable合约中使用这个体系结构+ OpenZppelin的initializer()修饰符。这确保了函数只能被调用一次。
-
使实现合约不可用:通过在构造函数中赋值isBase=true,并在initialize()函数中要求isBase== false,我们可以确保没有人可以篡改合约。该合约的唯一目的是充当逻辑合约,如果有人试图调用基础合约的初始化函数,它将立即恢复。
一旦我们准备好了Implementation.sol,让我们创建CloneFactory.sol。为此,我们将使用 OpenZeppelin 的 Clone 库中的 clone() 函数。
让我们来分析一下:
interface Implementation→initialize()是我们在Implementation.sol中需要的唯一函数。一旦创建了克隆合约,我们将立即调用它。
address public implementation→implementation .sol的地址。
mapping(address => address[]) public allClone →这只是一个跟踪所有已部署克隆的映射,第一个地址是 msg.sender 或克隆的所有者。
clon(address implementation)→这个函数来自Open Zeppelin。在较高的层次上,我们提供实现地址(implementation .sol),它返回该地址的一个实例,换句话说,它返回一个完全相同的implementation .sol的克隆。
clone()→这是用户将要调用的函数。一旦有人调用这个函数,第一件事就是创建一个新的克隆并将它保存在地址 sameChild 下。这个地址将持有与Implementation.sol相同的逻辑,但有自己的存储状态。正如我们在clone()的第三行中看到的,我们正在调用initialize函数:
这是至关重要的,这就是为什么我们要立即调用它。这将使clone()函数的调用者成为克隆合约的所有者。一旦做了,就没有回头路了。
用户可以克隆任意数量的合约,每个合约都有自己的存储空间。任何调用_clone()函数的人都将是唯一能够访问更改“x”的帐户。
结论
当我们需要为一个合约创建多个副本时,最小代理标准非常有效,每个副本都有自己的存储状态。最常见的用例是:Multi-Sig-Wallet、托管账户、某种类型的流动性池等。
作为提醒,无论何时执行此标准,都要特别注意以下事项:
-
实现合约中的initialize()函数只能被调用一次。
-
需要尽快触发实现合约中的initialize()函数。
-
使实现合约不可用。