本文详细介绍了MetaProxy标准,它是一种用于创建智能合约克隆的最小字节码实现,允许每个克隆附加不可变的元数据。文章还展示了如何使用MetaProxy标准创建ERC20合约,并解释了字节码的结构和如何操作元数据。
最小代理标准允许我们参数化克隆的创建,但这需要额外的初始化交易。完全可以绕过这一步,将我们关心的值参数化在代理的字节码中,而不是使用存储。
MetaProxy 标准也是一种最小字节码实现,用于创建智能合约克隆,并为每个克隆增加了唯一的不可变元数据。
这些元数据可以是任何东西,从字符串到数字,并且可以具有任意长度。然而,预期的用途是作为函数参数,用于参数化实现合约的行为。
由于该标准的字节码是已知的,因此用户和第三方工具(如 Etherscan)可以借此确定代理总是会重定向到特定的实现合约地址以及附加的元数据。
让我们来看看没有元数据的 MetaProxy 的字节码。
600b380380600b3d393df3363d3d373d3d3d3d60368038038091363936013d73bebebebebebebebebebebebebebebebebebebebe5af43d3d93803e603457fd5bf3
MetaProxy 字节码的长度为 65 字节,其中包括 11 字节的初始化代码和 54 字节的运行时代码。
尽管 MetaProxy 合约的字节码类似于最小代理标准,但字节码的某些部分是不同的,例如,下面字节码的绿色部分包含一些额外的操作码命令,我们将在后面进行解释。
有一个虚拟地址 0xbebebebebebebebebebebebebebebebebebebebe
将在部署后替换为实现合约地址。
本文由 Jesse Raymond (LinkedIn, Twitter) 共同撰写,作为 RareSkills 技术写作计划的一部分。
在本节中,我们将创建一个 ERC20 合约的 MetaProxy 克隆。让我们深入了解如何做到这一点,并直观展示元数据如何添加到克隆中。
为了实现 ERC20 合约,我们将继承 OpenZeppelin 的 ERC20Upgradeable
合约,该合约具有用于初始化 ERC20 状态变量的 “ERC20_init” 函数,而不是构造函数,因为在我们所构建的代理模式下,构造函数无法使用。
这是因为构造函数是在合约部署时调用的,如果我们遵循此方法,ERC20 标准的状态变量 name
和 symbol
将不会在 ERC20 MetaProxy 克隆字节码中初始化,因为构造函数将设置实现合约的存储,而不是克隆的存储。
然而,我们不会使用初始化函数,因为可以简单地从字节码中获取 ERC20 MetaProxy 克隆的 name
、symbol
和 totalSupply
,将它们作为元数据添加进来。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
contract ERC20Implementation is ERC20Upgradeable {
// 从元数据中获取 ERC20 名称
function name()
public
view
virtual
override
returns (string memory name__)
{
(name__, , ) = getMetadata();
}
// 从元数据中获取 ERC20 符号
function symbol()
public
view
virtual
override
returns (string memory symbol__)
{
(, symbol__, ) = getMetadata();
}
// 从元数据中获取 ERC20 总供应量
function totalSupply()
public
view
virtual
override
returns (uint256 totalSupply_)
{
(, , totalSupply_) = getMetadata();
}
// 铸造函数
function mint(uint amount) public {
_mint(msg.sender, amount * 10 ** 18);
}
/// 返回此 (ERC20 MetaProxy) 合约解码的元数据。
function getMetadata()
public
pure
returns (
string memory name__,
string memory symbol__,
uint256 totalSupply__
)
{
bytes memory data;
assembly {
let posOfMetadataSize := sub(calldatasize(), 32)
let size := calldataload(posOfMetadataSize)
let dataPtr := sub(posOfMetadataSize, size)
data := mload(64)
mstore(64, add(data, add(size, 32)))
mstore(data, size)
let memPtr := add(data, 32)
calldatacopy(memPtr, dataPtr, size)
}
// 返回解码后的元数据
return abi.decode(data, (string, string, uint256));
}
}
getMetadata
函数在实现中用于返回克隆的元数据。由于 MetaProxy 在其函数被调用时总是加载其元数据(这是标准的设计,我们将在本文后面进行解释),因此使用 getMetadata
函数提取调用中的元数据并将其作为元组返回。
它还在 ERC20 的 name
、symbol
和 totalSupply
函数中使用,以获取元数据的特定部分,无论是字符串 name
和 symbol
还是 uint256 的 totalSupply
。
我们从示例实现中推导了此函数并进行了修改,以适应我们的 ERC20 合约。
原始 EIP 还包含一个 链接 到 MetaProxyFactory
的实现,我们在此导入并继承。
MetaProxyFactory
包含用于创建新 MetaProxy 克隆的代码。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./ERC20Implementation.sol";
import "./MetaProxyFactory.sol";
contract ERC20MetaProxyFactory is MetaProxyFactory {
address[] public proxyAddresses;
function createClone(
string memory _name,
string memory _symbol,
uint256 _initialSupply
) public returns (address) {
// 编码 ERC20 构造函数参数
bytes memory metadata = abi.encode(_name, _symbol, _initialSupply);
// 创建代理
address proxyAddress = _metaProxyFromBytes(
address(new ERC20Implementation()),
metadata
);
proxyAddresses.push(proxyAddress);
return proxyAddress;
}
}
ERC20MetaProxyFactory
是我们创建新克隆的工厂合约。我们使用从 MetaProxyFactory
继承的 _metaProxyFromBytes
函数来部署新克隆。
_metaProxyFromBytes
函数接受两个参数,即:1. 实现合约的地址(这就是我们首先使用 new 关键字来部署 "ERC20Implementation" 合约的原因)。2. 元数据。
由于智能合约的字节码在代码中以十六进制表示,因此在将元数据附加到克隆的字节码之前,元数据必须先进行 abi 编码。
因此,我们在将 createClone
函数的参数作为元数据传递给 _metaProxyFromBytes
函数之前,先对它们进行编码,后者创建了新克隆并返回地址。
这是 _metaProxyFromBytes
函数的函数签名。
function _metaProxyFromBytes (address targetContract, bytes memory metadata) internal returns (address) {
// 部署新克隆的代码
}
以下是一个硬hat脚本,用于在 sepolia 网络上部署合约并与已部署的克隆进行交互:
const hre = require("hardhat");
async function main() {...
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!