[Solidity] Uniswap V2 Contract 코드 분석 1 - Factory

2022. 2. 24. 16:28Solidity

유니스왑의 컨트랙트는 크게 Core(Factory, Pairs), Periphery(Library, Router)로 구성돼있는것 같다.(https://uniswap.org/docs/v2/protocol-overview/smart-contracts/)

 오늘은 먼저 Core 코드를 분석해보자.
Core는 크게 Factory와 Pairs로 나뉜다. Factory 컨트랙트는 pool을 만드는 컨트랙트라고 한다. 또한 하나의 token pair마다 하나의 컨트랙트가 할당되는 것으로 보아, UNI-ETH pair에 유니크한 컨트랙트가 하나 할당됐고, 다른 pair에도 각자 유니크한 컨트랙트가 할당됐다고 보면 될것같다.

 Pairs 컨트랙트는 AAM(Automated Market Maker)를 제공하고, pool에 존재한믄 토큰의 비율을 추적하는 역할을 한다.

Uniswap의 Factory interface를 정의한 IUniswapV2Factory 의 내용이다. 하나씩 살펴보자.

pragma solidity =0.5.16;

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

contract UniswapV2Factory is IUniswapV2Factory {
    address public feeTo; // 이 pool을 이용하는 사람들이 수수료를 지불하는 지갑 또는 컨트랙트 주소
    address public feeToSetter; // 위의 feeTo를 설정할 수 있는 주소(아마도 관리자)

    // getPair 라는 변수는 이더리움 address를 key, map(address => address)를 value로 갖는 map이다. 
    // (map은 키-밸류로 이루어진 튜플 형식의 변수를 뜻한다. 간단히 표를 생각하면 편하다.) 중첩된 map 변수인데, 
    // 처음 봤을때는 뭐하는 변수인지 잘 몰랐지만 함수에서 쓰임새를 살펴보니 erc20 토큰 두 개를 key로 순서대로 입력하면
    // pair 컨트랙트 주소가 나오는 변수였다. (즉 token A address => (token B address => pair address) 모양이라고 생각하면 될 것 같다.
    mapping(address => mapping(address => address)) public getPair; // 파라미터로 들어온 두 토큰(erc20) pair pool의 컨트랙트 주소

    address[] public allPairs; // 모든 토큰 pool의 컨트랙트 주소, 이름은 allPairs인데 파라미터로 들어온 인덱스에 해당하는 컨트랙트를 반환하는 것 같다

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

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

    // 전체 pool의 개수
    function allPairsLength() external view returns (uint) {
        return allPairs.length;
    }

    // 파라미터로 들어온 두 토큰 pair로 이루어진 pool을 만든다.
    function createPair(address tokenA, address tokenB) external returns (address pair) {
        // tokenA의 주소와 tokenB가 다른지 여부를 체크한다. (같은 토큰이면 실패)
        require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
        // address 형태의 token0, token1를 생성하고 tokenA와 tokenB를 작은 것 부터 대입한다.
        // 예를 들어 tokenA의 주소가 0, tokenB의 주소가 1이면 token0 = tokenA, token1 = tokenB가 된다.
        (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
        // token0가 zero address가 아닌지 체크한다. (zero address면 실패)
        require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
        // getPair 변수에 tokenA, tokenB를 key로 하여 value가 존재한다면, 이미 Uniswap에 존재하는 토큰 페어로 인식하여 실패한다. 즉, 처음으로 만들어지는 토큰 페어여야만 한다.
        require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
        // UniSwapV2Pair의 byte code를 가져온다. UniSwapV2Pair는 Core에 존재하는 또 다른 컨트랙트이며, 혹시 byte code가 뭔지 모르겠다면 Solidity로 만든 컨트랙트를 이더리움 네트워크가 인식할 수 있게 번역한(컴파일 생각하면 됨) 언어다.
        bytes memory bytecode = type(UniswapV2Pair).creationCode;

        // 두 주소 token 0, token 1를 encodePacked(정확히 어떤 연산인지는 더 알아봐야겠으나, solidity docs를 살펴보니 여러 데이터를 압축하는 기법)하여 압축한 뒤 keccak256 함수를 이용해 해시한다.
        bytes32 salt = keccak256(abi.encodePacked(token0, token1));
        assembly {
            // create2에는 nonce가 파라미터로 들어가지 않는다. 함수에서 사용된 예제만 보더라도 4가지 parameter를 모두 직접 계산해서, 새로 만들어질 컨트랙트의 주소를 예측할 수 있다.
            // 새로 만들어지는 컨트랙트의 주소를 구할 수 있으니, getPair나 allPairs변수에도 즉시 컨트랙트 주소를 넣을 수 있다. 만약 주소를 예측할 수 없는 create를 썼다면, 내가 배포한 컨트랙트 주소를 받아오기 전(트랜잭션이 컨펌돼야 하니까 아마 한참 후?)까지는 이 연산을 할 수 없을 것 같다.
            pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
        }
        IUniswapV2Pair(pair).initialize(token0, token1);
        // getPair 맵에 token0 - token1, token1 - token 0를 key, 새로 만들어진 pair 컨트랙트 주소를 value로 대입한다. 굳이 키를 바꿔가면서 하는 이유는 뭘 먼저 대입해도 값이 나오도록 하기 위해...
        getPair[token0][token1] = pair;
        getPair[token1][token0] = pair; // populate mapping in the reverse direction
        // allPairs 리스트에 새로 만들어진 주소를 넣는다.
        allPairs.push(pair);
        // PairCreated 이벤트를 emit한다.
        emit PairCreated(token0, token1, pair, allPairs.length);
    }

    // feeTo를 설정하는 함수
    function setFeeTo(address _feeTo) external {
        require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
        feeTo = _feeTo;
    }

    // feeToSetter를 설정하는 함수
    function setFeeToSetter(address _feeToSetter) external {
        require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
        feeToSetter = _feeToSetter;
    }
}

'Solidity' 카테고리의 다른 글

[Solidity] Vault Contract  (0) 2022.07.26