Solidityプロキシ/実装モードでのコントラクトコールバック関数の使用
Solidityでは、
fallback
関数呼び出しが対応する関数と一致しない場合に呼び出される関数がコントラクトに含まれていることがわかっています。私たちが通常使用するプロキシ/実装パターンは、そのような機能に基づいています。プロキシコントラクトには存在しないプロキシコントラクトfallback
の関数を呼び出すため、関数を介してコントラクトを実装する対応する関数(コードスニペット)を呼び出して、デリゲートを呼び出します。ただし、対応する関数呼び出しが実装コントラクトで一致しない場合はどうなりますか?このとき、fallback
コントラクトを実装する関数を呼び出します(定義されている場合)。このfallback
関数では、機能拡張を取得するために、コントラクトを通常どおりにコーディングできます。
1.アプリケーションシナリオ
0.6.0未満はfallback
関数と呼ばれないため、この記事はSolidity0.6.0以降に基づいています。
そのようなシナリオがあり、契約Aをプロキシし、契約Bを実装するとします。そのような一連の契約があります:C、D、E、F、G…。待って。それらはすべてプロキシコントラクトの関数を呼び出します。これらの関数の名前は異なりますが、処理は同じです。コントラクトAはエージェントであるため、実装コード自体はなく、すべての実装コードはBにあります。BにC、D、Eに対応する関数呼び出しを実装する場合、最初の問題は、次に、新しいCCおよびDDコントラクトがある場合はどうなるでしょうか。この時点で、fallback
コントラクトBの機能を柔軟に適用して、これらの呼び出しを均一に処理できます。
コントラクト関数が呼び出されるとき、一般的に重要なパラメーターは呼び出し元(msg.sender)と呼び出しパラメーター(payload)です。このfallback
関数にはパラメーターがないため、パラメーターを直接渡すことはできません。では、渡された関数パラメーターをどのように取得するのでしょうか。コントラクト間の関数呼び出し(または外部アカウント呼び出しコントラクト)は、payload
送信用の呼び出しパラメーターをエンコードすることを知っています。通常の関数を呼び出すと、EVMは自動的にそれを解析します。このような関数をパラメーターなしで呼び出す場合fallback
、関数はパラメータータイプを定義しないため、手動で解析する必要があります。ここでは、UniswapV3のメソッドを使用して解析します。
ここで少し余談です。通常、関数をエンコードする場合、各関数パラメーターは1つword
、つまり256ビット64バイトに拡張されるためです。また、通常使用するデータのほとんどは64バイト未満です。たとえば、アドレスは40バイトしか使用せず、最初の24バイトが拡張され0
、これらの拡張は0
関数が呼び出されたときにガス計算にも組み込まれます。はい、しかしこれら0
は私たちのデータには本当に役に立たないです。膨張0が多い場合、ガス利用率に大きく影響します。そのため、UniswapV3はガスを節約するために圧縮符号化方式を採用しています。もちろん、圧縮されてエンコードされているので、対応するデコード機能も提供されているので、直接使用できます。
2.契約書のサンプル
これが私たちが書いた契約の例です:
FallbackTest.sol
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
import "hardhat/console.sol";
interface IProxy {
function setGreeting(uint value) external;
}
contract Test {
address public proxy;
constructor(address _proxy) {
proxy = _proxy;
}
function test(uint value) public {
console.log("in test function:");
IProxy(proxy).setGreeting(value);
}
}
contract Proxy {
address public impl; //这里应该指定插槽,这里简化了。
constructor(address _impl) {
impl = _impl;
}
fallback () external payable virtual {
console.log("in proxy fallback");
_delegate(impl);
}
function _delegate(address implementation) internal virtual {
// solhint-disable-next-line no-inline-assembly
assembly {
// Copy msg.data. We take full control of memory in this inline assembly
// block because it will not return to Solidity code. We overwrite the
// Solidity scratch pad at memory position 0.
calldatacopy(0, 0, calldatasize())
// Call the implementation.
// out and outsize are 0 because we don't know the size yet.
let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
// Copy the returned data.
returndatacopy(0, 0, returndatasize())
switch result
// delegatecall returns 0 on error.
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}
contract Impl {
address public impl_address; //未使用,用来防止插槽共享冲突
address public caller;
//演示获取的msg.sender及相应数据
fallback () external payable virtual {
console.log("in impl fallback");
caller = msg.sender;
bytes memory data = msg.data;
uint value = toUint256(data,4);
console.log("value:", value);
deal(value);
}
function toUint256(bytes memory _bytes, uint256 _start) internal pure returns (uint24) {
require(_start + 32 >= _start, 'toUint256_overflow');
require(_bytes.length >= _start + 32, 'toUint256_outOfBounds');
uint24 tempUint;
assembly {
tempUint := mload(add(add(_bytes, 0x20), _start))
}
return tempUint;
}
function deal(uint value) internal {
console.log("in impl deal function:",value);
}
}
コードからわかるように、開発にはHardhatツールを使用します。テストコントラクトを使用してプロキシコントラクトを呼び出します。プロキシコントラクトは呼び出しを実装コントラクトに委任し、実装コントラクトfallback
は関数内の対応するデータを解析して、それを処理関数に渡します。console.log
ログを出力して、コールフロー全体を表示するために使用します。
Impl
ここで、コントラクトの最初の状態変数に未使用の状態変数を指定していることに注意してください。これは、プロキシコントラクトの最初のスロットに対応する状態変数がであるためaddress public impl
、実現コントラクトの最初の状態変数(つまり、最初のスロット、つまりslot0)がプロキシコントラクトと共有されるためです(ここで確認できます)。コントラクトの内部は実際にはスロット位置に従って変数にアクセスされ、定義された変数位置部分がスロット位置を決定します)。したがってimpl_address
、最初に最初のスロット(Slot0)を占有するように定義し、公式に使用する変数は2番目の変数呼び出し元(スロット1)から開始します。
私たちのtoUint256
関数はUniswapV3から変更されていますtoUint24
。uint24、3バイトであることが判明しました。uint256、32バイトに変更しました。tempUint := mload(add(add(_bytes, 0x20), _start))
これは、10進数の32が16進数であるため0x20
です。
3.テストスクリプト
契約書が作成されたので、テストしてみましょう。
対応するテストスクリプトを記述しsample-test.js
ます。
const {
expect } = require("chai");
const {
ethers } = require("hardhat");
describe("FallbackTest", function () {
it("Should be Success", async function () {
const Impl = await ethers.getContractFactory("Impl");
const Proxy = await ethers.getContractFactory("Proxy");
const Test = await ethers.getContractFactory("Test");
const impl = await Impl.deploy();
await impl.deployed();
console.log("impl:",impl.address)
const proxy = await Proxy.deploy(impl.address)
await proxy.deployed();
console.log("proxy:",proxy.address)
const test = await Test.deploy(proxy.address)
await test.deployed()
console.log("test:",test.address)
let instance = await Impl.attach(proxy.address)
expect(instance.address).to.equal(proxy.address)
expect(await instance.caller()).to.equal(ethers.constants.AddressZero);
const testTx = await test.test(12);
await testTx.wait();
expect(await instance.caller()).to.equal(test.address);
});
});
次に、スクリプトを実行します。npx hardhat test
出力は次のようになります。
FallbackTest
impl: 0x5FbDB2315678afecb367f032d93F642f64180aa3
proxy: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
test: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
in proxy fallback
in test function:
in proxy fallback
in impl fallback
value: 12
in impl deal function: 12
in proxy fallback
上記の出力は、コントラクトの実行プロセスを検証し、対応するパラメーター値を正しく解析します12
。
4.拡張
上記の例を見るだけでは、多くのアプリケーションシナリオがない可能性があります(そのうちの1つは、複数のUniswapV2のような交換のフラッシュローンコールバックインターフェイスです)。しかし、コントラクトを実装するfallback
関数では、別の実装コントラクトを再度呼び出すように委任できますが、それは魔法ではありませんか?つまり、プロキシコントラクトは複数の実装コントラクトを持つことができます。この状況は通常、契約が大きすぎて代理契約で処理できない場合に使用されます。ただし、この場合、最初に複数の実装コントラクトを使用することを考えるのではなく、機能をサブコントラクトに分割して再設計することを検討する方が重要です。