2025.01.15
Minimal Proxy Contract(EIP-1167)の活用
こんにちは。次世代システム研究室のL.W.です。
最近Minimal Proxy Contract(EIP-1167)を試してみましたが、いろんなところで使えるかと思って、共有します。
1.EIP-1167
ERC-1167 は、Minimal Proxy Contract(ミニマルプロキシコントラクト)(Openzeppelinのコードでは「Clone」と呼ばれる)をデプロイするための標準です。
Minimal Proxy Contractのruntime codeは「0x3d602d80600a3d3981f3363d3d373d3d3d363d73 + implementation + 5af43d82803e903d91602b57fd5bf3」という式となります。
利用者からすると、Minimal Proxy Contractをアクセスするとき、Minimal Proxy Contract自体がimplementationのロジックで実行させます。
これにより、implementationでのロジックのような同じコードを何度もデプロイする必要がなくなり、ストレージの重複が避けられ、デプロイコストも低くなります。
このアプローチは、特に多くの同様のロジックを持つコントラクトをデプロイする場合に非常に有用で、開発者にとって経済的かつ効率的なソリューションを提供します。
2.EIP-1167の活用の例
ユーザごとに、アドレスを生成し、ユーザへ提供します。ユーザはこのアドレスにERC20送金させます。入金が確認できれば、アドレスからERC20トークンを回収します。
これらのアドレスをProxy化できます。
Proxyの作成するFactoryコントラクを自作します。ユーザIDなどをsaltとして、Proxy作成できます。
Minimal Proxy Contractは自作ではなく、Openzeppelinの実装を活用できます。

2.1. ロジックのImplementとProxyの製造工場のコントラクトをデプロイする

Implementの製造工場のコントラクトのコードは以下の通りです。
pragma solidity 0.8.4;
interface IERC20 {
function transfer(address recipient, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
contract Implementation{
function withdraw(address erc20TokenAddress, address recipient) external {
// the target erc20 token address
IERC20 erc20Token = IERC20(erc20TokenAddress);
// get balance
uint256 balance = erc20Token.balanceOf(address(this));
// transfer
if (balance > 0) {
require(erc20Token.transfer(recipient, balance), "Transfer failed");
}
}
}
Proxyの製造工場のコントラクトのコードは以下の通りです。
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.4;
import "@openzeppelin/contracts/proxy/Clones.sol";
contract FactoryClones {
using Clones for address;
function cloneDeterministic(address implementation, bytes32 salt) external returns (address) {
return implementation.cloneDeterministic(salt);
}
function predictDeterministicAddress(
address implementation,
bytes32 salt,
address deployer
) external pure returns (address){
return implementation.predictDeterministicAddress(salt, deployer);
}
function predictDeterministicAddress(
address implementation,
bytes32 salt
) external view returns (address){
return implementation.predictDeterministicAddress(salt);
}
}
2.2. 実行
Optimism sepoliaネットワークにデプロイ見ました。
FactoryClones
Implement
一つのProxyをデプロイしました。掛かったガスは63,233となりました。高くないですね。
Minimal Proxyではない場合、一つのproxyのデプロイでどのぐらいガスが掛かるか。
import "@openzeppelin/contracts/proxy/Clones.sol";
import "./Implementation.sol";
contract FactoryClones {
using Clones for address;
function cloneDeterministic(address implementation, bytes32 salt) external returns (address) {
return implementation.cloneDeterministic(salt);
}
function newCreate() external returns (address) {
address wallet = address(new Implementation());
return wallet;
}
... ...
}
newCreateの呼び出しで、173,316が掛かりました。Minimal Proxyの三倍ぐらいですね。
ユーザからProxyへ1GYENの着金を確認できたら、この1GYENを回収しました。
2.3. トークン送金で見つかった問題
上のImplementationコントラクトを絶対に本番に利用しないでください。誰でも
contract Implementation{
// fix to address.
// 誰で呼び出しても我々指定しておいたアドレスへ送金
address private constant RECIPIENT_ADDRESS = 0xE6b48d76Bc4805ABF61F38A55F1D7C362c8BfdA8;
function withdraw(address erc20TokenAddress) external {
// the target erc20 token address
IERC20 erc20Token = IERC20(erc20TokenAddress);
// get balance
uint256 balance = erc20Token.balanceOf(address(this));
// transfer
if (balance > 0) {
require(erc20Token.transfer(RECIPIENT_ADDRESS, balance), "Transfer failed");
}
}
}
2.3. ETHの自動転送
コードは以下の通りです。
// SPDX-License-Identifier: MIT
pragma solidity 0.8.4;
interface IERC20 {
function transfer(address recipient, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
contract Implementation{
// fix to address
address private constant RECIPIENT_ADDRESS = 0xE6b48d76Bc4805ABF61F38A55F1D7C362c8BfdA8;
receive() external payable {
(bool success, ) = RECIPIENT_ADDRESS.call{value: msg.value}("");
require(success, "Forwarding funds failed");
}
}
Proxyをデプロイしました。
https://sepolia-optimism.etherscan.io/address/0x9b6a2c8665d72fff4a3b963c14a923d4f0810d74#code
このProxyアドレスにETHを送金すると、Proxyを通して事前し設定しておいたアドレスへ転送できました。
ユーザが掛かったガスが35,485となります。これに対して、一般的なEOAアドレスに送金する場合、21,000がかかります。
2.3. Openzeppelinの実装したEIP-1167の最新版
最新のバージョンの0.5xでは、以下の方法を新規に追加されました。
cloneWithImmutableArgsおよびcloneDeterministicWithImmutableArgs:
これらのバリアントは、インスタンスごとの不変な引数を持つクローンを作成するためのものです。cloneWithImmutableArgs: 通常のクローンを作成し、不変な引数を保持します。cloneDeterministicWithImmutableArgs: 確定的なクローンを作成し、同様に不変な引数を保持します。
fetchCloneArgs:
このメソッドを使用して、クローンインスタンスに関連付けられた不変な引数を取得できます。-
predictDeterministicWithImmutableArgs:
不変な引数に基づいて確定的な予測を行うための関数です。
/**
* @dev Deploys and returns the address of a clone that mimics the behavior of `implementation` with custom
* immutable arguments. These are provided through `args` and cannot be changed after deployment. To
* access the arguments within the implementation, use {fetchCloneArgs}.
*
* This function uses the create opcode, which should never revert.
*/
function cloneWithImmutableArgs(address implementation, bytes memory args) internal returns (address instance) {
return cloneWithImmutableArgs(implementation, args, 0);
}
/**
* @dev Same as {xref-Clones-cloneWithImmutableArgs-address-bytes-}[cloneWithImmutableArgs], but with a `value`
* parameter to send native currency to the new contract.
*
* NOTE: Using a non-zero value at creation will require the contract using this function (e.g. a factory)
* to always have enough balance for new deployments. Consider exposing this function under a payable method.
*/
function cloneWithImmutableArgs(
address implementation,
bytes memory args,
uint256 value
) internal returns (address instance) {
if (address(this).balance < value) {
revert Errors.InsufficientBalance(address(this).balance, value);
}
bytes memory bytecode = _cloneCodeWithImmutableArgs(implementation, args);
assembly ("memory-safe") {
instance := create(value, add(bytecode, 0x20), mload(bytecode))
}
if (instance == address(0)) {
revert Errors.FailedDeployment();
}
}
/**
* @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation` with custom
* immutable arguments. These are provided through `args` and cannot be changed after deployment. To
* access the arguments within the implementation, use {fetchCloneArgs}.
*
* This function uses the create2 opcode and a `salt` to deterministically deploy the clone. Using the same
* `implementation`, `args` and `salt` multiple times will revert, since the clones cannot be deployed twice
* at the same address.
*/
function cloneDeterministicWithImmutableArgs(
address implementation,
bytes memory args,
bytes32 salt
) internal returns (address instance) {
return cloneDeterministicWithImmutableArgs(implementation, args, salt, 0);
}
/**
* @dev Same as {xref-Clones-cloneDeterministicWithImmutableArgs-address-bytes-bytes32-}[cloneDeterministicWithImmutableArgs],
* but with a `value` parameter to send native currency to the new contract.
*
* NOTE: Using a non-zero value at creation will require the contract using this function (e.g. a factory)
* to always have enough balance for new deployments. Consider exposing this function under a payable method.
*/
function cloneDeterministicWithImmutableArgs(
address implementation,
bytes memory args,
bytes32 salt,
uint256 value
) internal returns (address instance) {
bytes memory bytecode = _cloneCodeWithImmutableArgs(implementation, args);
return Create2.deploy(value, salt, bytecode);
}
/**
* @dev Computes the address of a clone deployed using {Clones-cloneDeterministicWithImmutableArgs}.
*/
function predictDeterministicAddressWithImmutableArgs(
address implementation,
bytes memory args,
bytes32 salt,
address deployer
) internal pure returns (address predicted) {
bytes memory bytecode = _cloneCodeWithImmutableArgs(implementation, args);
return Create2.computeAddress(salt, keccak256(bytecode), deployer);
}
/**
* @dev Computes the address of a clone deployed using {Clones-cloneDeterministicWithImmutableArgs}.
*/
function predictDeterministicAddressWithImmutableArgs(
address implementation,
bytes memory args,
bytes32 salt
) internal view returns (address predicted) {
return predictDeterministicAddressWithImmutableArgs(implementation, args, salt, address(this));
}
/**
* @dev Get the immutable args attached to a clone.
*
* - If `instance` is a clone that was deployed using `clone` or `cloneDeterministic`, this
* function will return an empty array.
* - If `instance` is a clone that was deployed using `cloneWithImmutableArgs` or
* `cloneDeterministicWithImmutableArgs`, this function will return the args array used at
* creation.
* - If `instance` is NOT a clone deployed using this library, the behavior is undefined. This
* function should only be used to check addresses that are known to be clones.
*/
function fetchCloneArgs(address instance) internal view returns (bytes memory) {
bytes memory result = new bytes(instance.code.length - 45); // revert if length is too short
assembly ("memory-safe") {
extcodecopy(instance, add(result, 32), 45, mload(result))
}
return result;
}
/**
* @dev Helper that prepares the initcode of the proxy with immutable args.
*
* An assembly variant of this function requires copying the `args` array, which can be efficiently done using
* `mcopy`. Unfortunately, that opcode is not available before cancun. A pure solidity implementation using
* abi.encodePacked is more expensive but also more portable and easier to review.
*
* NOTE: https://eips.ethereum.org/EIPS/eip-170[EIP-170] limits the length of the contract code to 24576 bytes.
* With the proxy code taking 45 bytes, that limits the length of the immutable args to 24531 bytes.
*/
function _cloneCodeWithImmutableArgs(
address implementation,
bytes memory args
) private pure returns (bytes memory) {
if (args.length > 24531) revert CloneArgumentsTooLong();
return
abi.encodePacked(
hex"61",
uint16(args.length + 45),
hex"3d81600a3d39f3363d3d373d3d3d363d73",
implementation,
hex"5af43d82803e903d91602b57fd5bf3",
args
);
}
これをどのように活用できるか、正直まだ分かりません。これから深掘りして調査してみようと思います。
2.4. Same contract addresses on different EVM networks
L2のブロックチェーンがどんどん出て来ていますね。
異なるL2チェーンでも同じコントラクトアドレスで異なるロジック(内部実現)も可能です。
EIP1167と同じでproxyを利用しています。
以下のcreate3というコントラクトが使えるかと思います。(auditではないので、リスクがある可能性がある)
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.4;
import "@0xsequence/create3/contracts/Create3.sol";
import "./FactoryClones.sol";
contract Create3Deployer {
function deployChild(string calldata _salt) external {
Create3.create3(
keccak256(bytes(_salt)),
abi.encodePacked(
type(FactoryClones).creationCode,
abi.encode(
42,
msg.sender
)
)
);
}
function addressOfChild(string calldata _salt) external view returns (address) {
return Create3.addressOf(keccak256(bytes(_salt)));
}
}
実際にデプロイしたコントラクトは以下の通りです。
saltを”gmo”にして、FactoryClonesコントラクトのデプロイできました。
このアドレスのFactoryClonesコントラクトは同じsaltで異なるブロックチェーンでもデプロイできます。
アドレスはここで予測できます。
3.まとめ
このMinimal Proxy をpredictDeterministicAddress(implementation, salt)方法で事前に予測されますので、ガスを節約ために、ユーザにproxyを提示する場合、別にデプロイしておく必要がなくなります。ユーザからの着金を確定できてから、proxyをデプロイし、資金を回収できます。
そして、ETHの自動転送には向いています。回収の手間を抜けるし、ユーザの掛かった手数料も決して高くないでしょう。
ERC20の場合、自動転送の実現がだけかと思います。トークンを回収するために、二つの動作が要りますね。
proxy deploy(掛かるガス63,233)とwithdraw(掛かるガス51,501)です。手数料がかかりそうです。
4.最後に
次世代システム研究室では、グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。アプリケーション開発者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ募集職種一覧からご応募をお願いします。
皆さんのご応募をお待ちしています。
グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。
Follow @GMO_RD

