UniswapV2Factory-源码解读

  • Louis
  • 更新于 2024-09-03 16:58
  • 阅读 416

这一篇文章,从源码角度梳理UniswapV2Factory的实现

合约源码:

pragma solidity =0.5.16;

import './interfaces/IUniswapV2Factory.sol';
import './UniswapV2Pair.sol';

//uniswap工厂
contract UniswapV2Factory is IUniswapV2Factory {
    address public feeTo; //收税地址
    address public feeToSetter; //收税权限控制地址

    //配对映射, 地址 => (地址 => 地址)
    mapping(address => mapping(address => address)) public getPair;

    //所有配对数组
    address[] public allPairs;

    //配对合约的Bytecode的hash
    bytes32 public constant INIT_CODE_PAIR_HASH = keccak256(abi.encodePacked(type(UniswapV2Pair).creationCode));
    //事件:配对被创建
    event PairCreated(address indexed token0, address indexed token1, address pair, uint);

    /**
     * @dev 构造函数
     * @param _feeToSetter 收税开关权限控制
     */
    constructor(address _feeToSetter) public {
        feeToSetter = _feeToSetter;
    }

    /**
     * @dev 查询配对数组长度方法
     */
    function allPairsLength() external view returns (uint) {
        return allPairs.length;
    }

    /**
     *
     * @param tokenA TokenA
     * @param tokenB TokenB
     * @return pair 配对地址
     * @dev 创建配对
     */
    function createPair(address tokenA, address tokenB) external returns (address pair) {
        //确认tokenA不等于tokenB
        require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
        //将tokenA和tokenB进行大小排序,确保tokenA小于tokenB
        (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
        //确认token0不等于0地址
        require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
        //确认配对映射中不存在token0=>token1
        require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
        //给bytecode变量赋值"UniswapV2Pair"合约的创建字节码
        bytes memory bytecode = type(UniswapV2Pair).creationCode;
        //将token0和token1打包后创建哈希
        bytes32 salt = keccak256(abi.encodePacked(token0, token1));
        //内联汇编
        //solium-disable-next-line
        assembly {
            //通过create2方法布署合约,并且加盐,返回地址到pair变量
            pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
        }
        //调用pair地址的合约中的"initialize"方法,传入变量token0,token1
        IUniswapV2Pair(pair).initialize(token0, token1);
        //配对映射中设置token0=>token1=pair
        getPair[token0][token1] = pair;
        //配对映射中设置token1=>token0=pair
        getPair[token1][token0] = pair; // populate mapping in the reverse direction
        //配对数组中推入pair地址
        allPairs.push(pair);
        //触发配对成功事件
        emit PairCreated(token0, token1, pair, allPairs.length);
    }

    /**
     * @dev 设置收税地址
     * @param _feeTo 收税地址
     */
    function setFeeTo(address _feeTo) external {
        require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
        feeTo = _feeTo;
    }

    /**
     * @dev 收税权限控制
     * @param _feeToSetter 收税权限控制
     */
    function setFeeToSetter(address _feeToSetter) external {
        require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
        feeToSetter = _feeToSetter;
    }
}

逻辑梳理

这是 Uniswap V2 中的工厂合约 (UniswapV2Factory) 的 Solidity 代码。这个合约负责创建和管理代币对(pair),并且包含了一些关键的功能。

编译器已经被锁死了,使用的是0.5.16版本,这个版本中的solidity还会有溢出问题。这是相关技术背景。

UniswapV2Factory仅仅继承了接口合约,继承接口合约的目的是为了保证要实现内部的所有方法,如果不实现其中的方法就会报错,接口相当于一种约束。

接口合约源码实现:

pragma solidity ^0.5.6;

interface IUniswapV2Factory {
    event PairCreated(address indexed token0, address indexed token1, address pair, uint);

    function feeTo() external view returns (address);
    function feeToSetter() external view returns (address);

    function getPair(address tokenA, address tokenB) external view returns (address pair);
    function allPairs(uint) external view returns (address pair);
    function allPairsLength() external view returns (uint);

    function createPair(address tokenA, address tokenB) external returns (address pair);

    function setFeeTo(address) external;
    function setFeeToSetter(address) external;
}

变量:

  1. feeTo:收取手续费的地址。
  2. feeToSetter:控制 feeTo 地址的权限地址。
  3. getPair:一个映射,用于存储两个代币之间的配对合约地址。
  4. allPairs:一个数组,存储所有创建的配对合约地址。
  5. INIT_CODE_PAIR_HASH:配对合约的字节码哈希值,用于通过 create2 函数部署合约。

事件:

  • PairCreated:每当一个新的代币对被创建时触发,记录了代币对的两个代币地址及其配对合约地址。

构造函数

constructor(address _feeToSetter) public {
    feeToSetter = _feeToSetter;
}

构造函数接受一个 _feeToSetter 地址,用于初始化 feeToSetter 变量。可以在部署工厂合约的时候,传入一个初始化的部署参数,这个参数是一个地址类型,也就是_feeToSetter,然后_feeToSetter 被设置为管理员之后,可以设置feeTo的地址,最终的手续费会被打到这个feeTo地址上面。

方法

allPairsLength方法:

/**
 * @dev 查询配对数组长度方法
 */
function allPairsLength() external view returns (uint) {
    return allPairs.length;
}

这是一个view函数,返回 allPairs 数组的长度,即已经创建的配对数量。

createPair方法:

/**
 *
 * @param tokenA TokenA
 * @param tokenB TokenB
 * @return pair 配对地址
 * @dev 创建配对
 */
function createPair(address tokenA, address tokenB) external returns (address pair) {
    //确认tokenA不等于tokenB
    require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
    //将tokenA和tokenB进行大小排序,确保tokenA小于tokenB
    (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
    //确认token0不等于0地址
    require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
    //确认配对映射中不存在token0=>token1
    require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
    //给bytecode变量赋值"UniswapV2Pair"合约的创建字节码
    bytes memory bytecode = type(UniswapV2Pair).creationCode;
    //将token0和token1打包后创建哈希
    bytes32 salt = keccak256(abi.encodePacked(token0, token1));

    //内联汇编
    //solium-disable-next-line
    assembly {
        //通过create2方法布署合约,并且加盐,返回地址到pair变量
        pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
    }

    //调用pair地址的合约中的"initialize"方法,传入变量token0,token1
    IUniswapV2Pair(pair).initialize(token0, token1);

    //配对映射中设置token0=>token1=pair
    getPair[token0][token1] = pair;

    //配对映射中设置token1=>token0=pair
    getPair[token1][token0] = pair; // populate mapping in the reverse direction

    //配对数组中推入pair地址
    allPairs.push(pair);

    //触发配对成功事件
    emit PairCreated(token0, token1, pair, allPairs.length);
}
  • 输入:两个代币地址 tokenAtokenB
  • 输出:返回新创建的配对合约地址。

逻辑梳理:

  • 确保 tokenAtokenB 不相同。 按字典顺序将tokenAtokenB 排序,确保 token0 < token1, 因为是使用16进制表示,他们是可以进行比大小的。一定要将较小的token赋值给token0,稍微大的一个赋值给token1。
  • 确保 token0 不是零地址。这里有个巧妙的计算,小的那个不是0地址,大的那个肯定也不是0地址。
  • 确保这对代币的配对还没有被创建。
  • 使用 create2 创建新的 UniswapV2Pair 合约,并通过计算哈希值生成 salt,确保创建的合约地址是唯一的。
  • 初始化新合约。
  • 将新配对的地址存储到 getPair 映射和 allPairs 数组中。
  • 触发 PairCreated 事件。

setFeeTo方法:

/**
 * @dev 设置收税地址
 * @param _feeTo 收税地址
 */
function setFeeTo(address _feeTo) external {
    require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
    feeTo = _feeTo;
}

允许 feeToSetter 设置 feeTo 地址,控制手续费的接收地址。

setFeeToSetter方法:

/**
 * @dev 收税权限控制
 * @param _feeToSetter 收税权限控制
 */
function setFeeToSetter(address _feeToSetter) external {
    require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
    feeToSetter = _feeToSetter;
}

允许当前的 feeToSetter 地址设置新的 feeToSetter 地址。

关于UniswapV2Pair字节码的计算逻辑

bytes memory bytecode = type(UniswapV2Pair).creationCode;

这一行代码在 Solidity 中用于获取 UniswapV2Pair 合约的创建字节码(也就是部署合约时使用的字节码)。

详细解释

type(UniswapV2Pair):

  • 这是一个 Solidity 中的特殊关键字,用于获取某个合约类型的相关信息。UniswapV2Pair 是合约的名称。
  • 通过 type(ContractName),你可以访问与该合约相关的元信息,例如它的创建代码(creationCode)和运行时代码(runtimeCode)。

creationCode:

  • creationCode 是合约的创建字节码。这个字节码包含了合约的构造函数代码和所有初始的合约代码。当你部署一个合约时,以太坊虚拟机(EVM)会使用这个字节码来创建合约实例。
  • 换句话说,creationCode 是部署合约时发送给 EVM 的代码,EVM 执行这个代码后,最终会生成并存储合约的运行时代码。

bytes memory:

  • bytes 是一个动态字节数组类型,用于存储任意长度的字节序列。
  • memory 表示这个字节数组是在内存中分配的,而不是在区块链上存储(即不是 storage)。

具体含义

当你编写 bytes memory bytecode = type(UniswapV2Pair).creationCode; 时,实质上你是在获取 UniswapV2Pair 合约的部署字节码,并将其存储在 bytecode 变量中。

为什么需要这样做?

createPair 函数中,Uniswap V2 工厂合约需要动态部署一个新的 UniswapV2Pair 合约实例。这就需要使用 create2 操作码,而 create2 需要提供合约的创建字节码和一个 salt 值(一个随机或特定的哈希值),以确保新合约地址的唯一性和可预测性。

具体的操作流程如下:

  1. 获取创建字节码:通过 type(UniswapV2Pair).creationCode 获取 UniswapV2Pair 合约的创建字节码。
  2. 内联汇编部署合约:使用 create2 指令,并将获取的 bytecode 作为合约创建时的字节码输入。这确保了每次创建的 UniswapV2Pair 合约实例是正确的。

bytes memory bytecode = type(UniswapV2Pair).creationCode; 是在合约中动态获取 UniswapV2Pair 合约的创建代码,以便稍后使用 create2 操作码来部署新合约。这种方法在工厂模式中非常常见,因为工厂合约通常需要根据输入参数动态创建多个合约实例。

creationCoderuntimeCode区别是什么?

creationCoderuntimeCode 是合约代码在以太坊虚拟机(EVM)中的两个不同阶段的字节码。它们分别对应合约部署和运行时的代码。让我们来详细解释它们的区别。

creationCode (创建字节码)

定义creationCode 是合约的部署字节码。当你部署一个新的合约时,creationCode 被发送到 EVM 并执行,用来生成该合约的实例。

内容

  • 包含合约的构造函数代码(如果有的话),以及合约的初始化代码。
  • 包含所有初始化的静态数据和逻辑。
  • 在部署时,EVM 执行 creationCode,生成合约的 runtimeCode,并将 runtimeCode 存储在合约地址下。

使用场景

creationCode 只在合约创建时使用,一旦合约部署完成,它不会再被使用。

获取方式

  • 通过 type(ContractName).creationCode 可以获取某个合约的 creationCode

runtimeCode (运行时字节码)

定义runtimeCode 是合约在链上运行时的字节码。当外部账户或其他合约调用该合约时,EVM 执行的就是 runtimeCode

内容

  • 包含合约的业务逻辑,即合约的方法和事件定义。
  • 不包含构造函数的代码,因为构造函数只在合约部署时运行。

使用场景

runtimeCode 是合约部署后存在于链上的代码,每次调用合约时,EVM 都会执行这部分代码。

获取方式

在 Solidity 中并没有直接的方式获取 runtimeCode,因为 runtimeCode 是部署后的代码。可以通过 EVM 调用 extcodesizeextcodecopy 指令来获取。

举例说明

假设有一个简单的合约:

contract SimpleContract {
    uint256 public value;

    constructor(uint256 _value) public {
        value = _value;
    }

    function setValue(uint256 _value) public {
        value = _value;
    }
}

creationCode

包含设置初始值 value 的构造函数代码。 包含部署完成后生成 runtimeCode 的逻辑。

runtimeCode: 包含 setValue 方法和 value 变量的相关逻辑。 不包含构造函数,因为构造函数在部署时已经执行完毕。

内联汇编创建pair合约:

createPair 方法中使用了内联汇编来调用 create2 指令,以部署新的合约。这是一种更低级别的操作,使得合约创建时可以通过 salt 来保证唯一的合约地址。

//内联汇编
//solium-disable-next-line
assembly {
    //通过create2方法布署合约,并且加盐,返回地址到pair变量
    pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}

create2 操作码有四个参数。它们分别是:

create2(value, bytecode, size, salt)

value (指定为0 在这个例子中):

  • 类型:uint256
  • 向新创建的合约发送的以太币数量。通常是0,表示不附带任何以太币。

bytecode:

  • 类型:uint256(内存指针)
  • 新合约的创建字节码。这是实际部署合约时的代码,create2 使用它来生成新合约。

size:

  • 类型:uint256
  • 合约创建字节码的大小(以字节为单位)。这告诉 create2 应该从 bytecode 内存位置读取多少字节的数据。

salt:

  • 类型:bytes32
  • 一个随机或特定的 32 字节的值。这个值与 bytecode 共同决定了新合约的地址。不同的 salt 会生成不同的合约地址,即使 bytecode 相同。
点赞 1
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Louis
Louis
web3 developer,技术交流或者有工作机会可加VX: magicalLouis