Solidity代理/实现模式中实现合约回调函数的使用
Posted MateZero
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Solidity代理/实现模式中实现合约回调函数的使用相关的知识,希望对你有一定的参考价值。
Solidity代理/实现模式中实现合约回调函数的使用
我们知道,在Solidity中,合约有一个
fallback
函数,用于在函数调用时未匹配到相应的函数时调用。我们平常使用的代理/实现模式,正是基于这样一个功能。我们调用代理合约中的一个函数,该函数在代理合约中并不存在,于是通过fallback
函数来调用委托调用实现合约的相应函数(代码片断)。不过,当实现合约中也未匹配到相应的函数调用怎么办?这时,就是举调用实现合约的fallback
函数(假如定义了的话)。而在此fallback
函数里,我们可以正常的进行合约编码,从而得到功能上的拓展。
一、应用场景
本文基于Solidity 0.6.0以上,因为0.6.0以下并不叫fallback
函数。
假定有这么一个场景,代理合约A,实现合约B。 有这么一组合约:C,D,E,F,G…。等。他们都会调用代理合约一个函数,这些函数名称不同但是处理过程却相同。因为合约A为代理,其本身是没有实现代码的,所有实现代码都在B里。如果我们把C,D,E对应的函数调用都在B里的实现的话,第一麻烦,第二如果有新的CC,DD合约出现怎么办?此时,我们可以灵活应用合约B的fallback
函数,统一处理这些调用。
合约函数调用时,一般重要的参数有调用者(msg.sender)和调用参数(payload)。而fallback
函数是没有参数,所以无法直接进行参数传递。那么我们怎么获取传递的函数参数呢?我们知道,合约间的函数调用(或者外部账号调用合约),都是将调用参数编码为payload
进行传递。调用正常的函数时,EVM自动帮我们解析了。而调用fallback
这类没有参数的函数时,因为函数没有定义参数类型,所以需要手动解析。这里我们利用UniswapV3中的方法进行解析。
这里讲一点题外话,因为我们正常进行函数编码时,每个函数参数都被拓展为一个word
,也就是256位,64字节。而我们平常使用的绝大部分数据是用不到64字节的,例如地址只使用了40字节,前面的24字节会拓展为0
,而这些拓展的0
也是会并入函数调用时的gas计算的,但是这些0
对我们的数据其实是没有用处的。当有大量的拓展0出现时,会大大的影响gas利用率。因此,UniswapV3采用了一个压缩编码的方式来节省gas。既然压缩编码了,当然它也提供了相应的解码函数,我们直接拿过来使用即可。
二、示例合约编写
下面是我们编写的一个示例合约:
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工具进行开发。使用一个Test合约调用代理合约,代理合约再委托调用实现合约,实现合约在fallback
函数里解析相应数据,再传递给处理函数。我们使用console.log
来输出日志查看整个调用流程。
这里需要注意的是我们在Impl
合约里第一个状态变量指定了一个未使用的状态变量。这是因为代理合约的第一个插槽对应的状态变量为address public impl
,实现合约的第一个状态变量(也是在第一个插槽,也就是slot0)会和代理合约共享(这里可以看出合约内部其实是按插槽位置访问变量的,定义的变量位置部分的确定了插槽位置)。于是我们先定义了impl_address
来占用第一个插槽(Slot0),我们正式使用的变量从第2个变量caller开始(插槽1)。
我们的toUint256
函数从UniswapV3的toUint24
改动而来。原来是uint24,3个字节。我们改成uint256,32个字节。tempUint := mload(add(add(_bytes, 0x20), _start))
这里是因为十进制32是16进制0x20
。
三、测试脚本
合约编写完毕,我们来进行测试。
编写相应的测试脚本 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
。
四、拓展
单看上面的示例,应用的场景可能不多(其中一个场景为多个类UniswapV2交易所的交易对闪电贷回调接口)。但是我们在实现合约的fallback
函数里可以再次委托调用另一个实现合约,神奇不?也就是一个代理合约可以有多个实现合约。这种情况一般用于合约过大,一个代理合约处理不了的情况。但这种情况下更多的是考虑重新设计,拆分功能到子合约,而不是首先考虑使用多个实现合约。
以上是关于Solidity代理/实现模式中实现合约回调函数的使用的主要内容,如果未能解决你的问题,请参考以下文章