Smart contract:Smart contract is a program running on the ETH blockchain,which defines the logic behind the state changes on the blockchain.In abstract,smart contract is a rule that can be executed automatically.In real life,a contract needs to have a special execution role after the contract is formulated.Smart contract automates this step,and it will be executed only if the conditions specified in the smart contract are met.
Smart contracts are written in high-level languages(programming languages),such as Solidity or Vyper.Because the smart contract code is stored on the ETH blockchain,anyone can check the application logic of all smart contracts on the network.
首先调用UniswapV3Factory.getPool方法查看交易对是否已经创建,getPool函数是solidity自动为UniswapV3Factory合约中的状态变量getPool生成的外部函数,getPool的数据类型为:
contract UniswapV3Factory is IUniswapV3Factory,UniswapV3PoolDeployer,NoDelegateCall{
...
mapping(address=>mapping(address=>mapping(uint24=>address)))public override getPool;
...
}
使用3个map说明了v3版本使用(tokenA,tokenB,fee)来作为一个交易对的键,即相同代币,不同费率之间的流动池不一样。另外对于给定的tokenA和tokenB,会先将其地址排序,将地址值更小的放在前,这样方便后续交易池的查询和计算。
再来看UniswapV3Factory创建交易对的过程,实际上它是调用deploy函数完成交易对的创建:
function deploy(
address factory,
address token0,
address token1,
uint24 fee,
int24 tickSpacing
)internal returns(address pool){
parameters=Parameters({factory:factory,token0:token0,token1:token1,fee:fee,tickSpacing:tickSpacing});
pool=address(new UniswapV3Pool{salt:keccak256(abi.encode(token0,token1,fee))}());
delete parameters;
}
这里的fee和tickSpacing是和费率及价格最小间隔相关的设置,这里只关注创建过程,费率和tick的实现后面再来做介绍。
CREATE2
创建交易对,就是创建一个新的合约,作为流动池来提供交易功能。创建合约的步骤是:
pool=address(new UniswapV3Pool{salt:keccak256(abi.encode(token0,token1,fee))}());
这里先通过keccak256(abi.encode(token0,token1,fee)将token0,token1,fee作为输入,得到一个哈希值,并将其作为salt来创建合约。因为指定了salt,solidity会使用EVM的CREATE2指令来创建合约。使用CREATE2指令的好处是,只要合约的bytecode及salt不变,那么创建出来的地址也将不变。
关于使用salt创建合约的解释:Salted contract creations/create2
CREATE2指令的具体解释可以参考:EIP-1014。solidity在0.6.2版本后在语法层面支持了CREATE2.如果使用更低的版本,可以参考Uniswap v2的代码实现同样的功能。
使用CREATE2的好处是:
可以在链下计算出已经创建的交易池的地址
其他合约不必通过UniswapV3Factory中的接口来查询交易池的地址,可以节省gas
合约地址不会因为reorg而改变
不需要通过UniswapV3Factory的接口来计算交易池合约地址的方法,可以看这段代码。
新交易对合约的构造函数中会反向查询UniswapV3Factory中的parameters值来进行初始变量的赋值:
constructor(){
int24 _tickSpacing;
(factory,token0,token1,fee,_tickSpacing)=IUniswapV3PoolDeployer(msg.sender).parameters();
tickSpacing=_tickSpacing;
maxLiquidityPerTick=Tick.tickSpacingToMaxLiquidityPerTick(_tickSpacing);
}
为什么不直接使用参数传递来对新合约的状态变量赋值呢。这是因为CREATE2会将合约的initcode和salt一起用来计算创建出的合约地址。而initcode是包含contructor code和其参数的,如果合约的constructor函数包含了参数,那么其initcode将因为其传入参数不同而不同。在off-chain计算合约地址时,也需要通过这些参数来查询对应的initcode。为了让合约地址的计算更简单,这里的constructor不包含参数(这样合约的initcode将时唯一的),而是使用动态call的方式来获取其创建参数。
最后,对创建的交易对合约进行初始化:
function initialize(uint160 sqrtPriceX96)external override{
require(slot0.sqrtPriceX96==0,'AI');
int24 tick=TickMath.getTickAtSqrtRatio(sqrtPriceX96);
(uint16 cardinality,uint16 cardinalityNext)=observations.initialize(_blockTimestamp());
slot0=Slot0({
sqrtPriceX96:sqrtPriceX96,
tick:tick,
observationIndex:0,
observationCardinality:cardinality,
observationCardinalityNext:cardinalityNext,
feeProtocol:0,
unlocked:true
});
emit Initialize(sqrtPriceX96,tick);
}
初始化主要是设置了交易池的初始价格(注意,此时池子中还没有流动性),以及费率,tick等相关变量的初始化。完成之后一个交易池就创建好了。