管理合约(Manager Contract)
在部署我们的池子合约之前,仍然有一个问题需要解决。我们之前提到过,UniswapV3
合约由两部分构成:
- 核心合约(
core contracts
)实现了最核心的功能,不提供用户友好的交互接口 - 外围合约(
periphery contracts
)为核心合约实现了用户友好的接口
池子合约是核心合约,它并不需要用户友好或者实现灵活功能。它需要调用者进行所有的计算并且提供合适的参数。同时,它也没有使用ERC20的 transferFrom
函数来从调用者处转账,而是使用了两个 callback 函数:
uniswapV3MintCallback
,当铸造流动性的时候被调用,检查代币对的存款情况uniswapV3SwapCallback
,当交易token
的时候被调用,检查目标代币的存款情况
由于只有合约才能实现 callback
函数,池子合约并不能直接被普通用户(EOA
)调用。
我们下一步的目标是将这个池子合约部署在一个本地的区块链上,并且使用一个前端应用与其交互。 因此我们需要创建一个合约,能够让非合约的地址也与池子进行交互。让我们来实现吧!
工作流程
下面我们描述了管理合约的功能:
- 为了铸造流动性,我们需要
approve
对应的token
给管理合约; - 我们会调用管理合约的
mint
函数来铸造流动性,参数为铸造需要的参数以及池子的合约地址; - 管理合约会调用池子的
mint
函数,并且会实现uniswapV3MintCallback
。由于我们之前的approve
,管理合约会从我们的账户中把token
转到池子合约; - 为了交易
token
,我们也需要approve
对应的token
; - 我们会调用管理合约的
swap
函数,并且与mint
过程类似,它会调用池子合约的对应函数。管理者合约会把我们的token
转到池子中,池子进行对应的交易,然后把得到的token
发回给我们。
综上,管理合约主要作为用户和池子之间的中介来运行。
向 callback 传递数据
在实现管理合约之前,我们还需要更新我们的池子合约。
管理者合约需要能够与任何一个流动性池适配,并且能够允许任何地址调用它。为了达到这一点,我们需要对 callback
进行升级:
我们需要将池子的地址和用户的地址作为参数传递。下面是我们对于 uniswapV3MintCallback
之前的实现(测试合约中的):
function uniswapV3MintCallback(uint256 amount0, uint256 amount1) public {
if (transferInMintCallback) {
token0.transfer(msg.sender, amount0);
token1.transfer(msg.sender, amount1);
}
}
关键点:
- 这个函数转移的
token
是属于这个测试合约的——而我们希望使用transferFrom
来从管理合约的调用者出转出token
- 这个函数需要知道
token0
和token1
,但这两个变量会随着不同池子而变化。
想法:我们需要改变 callback
的参数,来将用户和池子的合约地址传进去。
接下来看一下 swap
的 callback
:
function uniswapV3SwapCallback(int256 amount0, int256 amount1) public {
if (amount0 > 0 && transferInSwapCallback) {
token0.transfer(msg.sender, uint256(amount0));
}
if (amount1 > 0 && transferInSwapCallback) {
token1.transfer(msg.sender, uint256(amount1));
}
}
同样,它从测试合约处转钱,并且已知 token0
和 token1
。
为了将这些参数传递给 callback
,我们需要首先将它们传递给 mint
和 swap
函数
(因为 callback
函数是被这两个函数调用的)。
然而,由于这些额外的数据并不会被上述两个函数本身使用,
为了避免参数的混淆,我们会使用 abi.encode() 来编码这些参数。
定义一下这些额外数据的结构:
// src/UniswapV3Pool.sol
struct CallbackData {
address token0;
address token1;
address payer;
}
...
接下来把编码后的数据传入 callback
:
function mint(
address owner,
int24 lowerTick,
int24 upperTick,
uint128 amount,
bytes calldata data // <--- New line
) external returns (uint256 amount0, uint256 amount1) {
IUniswapV3MintCallback(msg.sender).uniswapV3MintCallback(
amount0,
amount1,
data // <--- New line
);
...
}
function swap(address recipient, bytes calldata data) // <--- `data` added
public
returns (int256 amount0, int256 amount1)
{
IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(
amount0,
amount1,
data // <--- New line
);
...
}
现在,我们可以在测试合约的 callback 函数中解析对应的数据:
function uniswapV3MintCallback(
uint256 amount0,
uint256 amount1,
bytes calldata data
) public {
if (transferInMintCallback) {
UniswapV3Pool.CallbackData memory extra = abi.decode(
data,
(UniswapV3Pool.CallbackData)
);
IERC20(extra.token0).transferFrom(extra.payer, msg.sender, amount0);
IERC20(extra.token1).transferFrom(extra.payer, msg.sender, amount1);
}
}