체인의정석

UniswapV2 정리 2 - Uniswap V2 Factory 본문

블록체인/디파이

UniswapV2 정리 2 - Uniswap V2 Factory

체인의정석 2022. 10. 26. 14:36
728x90
반응형

https://it-timehacker.tistory.com/339

 

UniswapV2 정리 - UniswapV2Pair & UniswapV2ERC20 컨트렉트

https://github.com/Uniswap/v2-core/tree/master/contracts GitHub - Uniswap/v2-core: 🎛 Core smart contracts of Uniswap V2 🎛 Core smart contracts of Uniswap V2. Contribute to Uniswap/v2-core developm..

it-timehacker.tistory.com

 

Uniswap V2 Factory의 경우 1에서 살펴본 UniswapV2Pair에 대한 변수나 정보를 관리해주는 함수이다.

 

일단 UniswapV2 Factory는 

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

contract UniswapV2Factory is IUniswapV2Factory {

인터페이스도 따로 만들어서 관리중이고 V2Pair도 만들어서 사용중이다.

 

일단 인터페이스부터 봐야겠다.

pragma solidity >=0.5.0;

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;
}

일단 모든 함수에 이벤트로그를 다 남기지는 않지만 이벤트 정의는 인터페이스에서 하는 형태를 취하고 있다.

그리고 모든 함수 자체가 원래 external로 되어 있는데, 이렇게 되어 있는 이유는 한번 찾아보니

1. 가스비 절감 효과가 있다.

2. 외부에서 컨트렉트를 만들때 상속해서 쓸 수 있기 때문에 혼동을 방지 할 수 있다.

 

이런 2가지의 큰 이유가 있다고 한다.

 

pulbic만 쓰는 나로서는 신기하기도 했고 external과 public도 구분해서 써봐야 겠다는 생각이 들었다.

 

    address public feeTo;
    address public feeToSetter;

    mapping(address => mapping(address => address)) public getPair;
    address[] public allPairs;

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

일단 맨 위에 데이터를 정의하는 부분부터 보자면 feeTo는 유니스왑에서 걷는 수수료의 양이며 feeToSetter는 수수료를 산정하는 관리자의 값이다. 생성자에서 feeToSetter를 정의해 준다.

 

관련된 함수 먼저 보겠다.

    function setFeeTo(address _feeTo) external {
        require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
        feeTo = _feeTo;
    }

    function setFeeToSetter(address _feeToSetter) external {
        require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
        feeToSetter = _feeToSetter;
    }
}

요건 그냥 수수료를 정해주는 부분과 Fee setter 수정해 주는 부분.

그리고 setFeeTo에 있는 조건문을 보면 트랜잭션을 보내는 애가 feeToSetter가 맞는지 확인해 보는 부분이 있다.

 

그거 외엔 배열 길이를 통해 LP 토큰이 몇 페어나 있는지 확인하는 부분이 있다.

    function allPairsLength() external view returns (uint) {
        return allPairs.length;
    }

핵심 코드는 createPair 정도 밖에 없었는데

    function createPair(address tokenA, address tokenB) external returns (address pair) {
        require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
        (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
        require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
        require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
        bytes memory bytecode = type(UniswapV2Pair).creationCode;
        bytes32 salt = keccak256(abi.encodePacked(token0, token1));
        assembly {
            pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
        }
        IUniswapV2Pair(pair).initialize(token0, token1);
        getPair[token0][token1] = pair;
        getPair[token1][token0] = pair; // populate mapping in the reverse direction
        allPairs.push(pair);
        emit PairCreated(token0, token1, pair, allPairs.length);
    }

일단 조건문으로 비교를 하고 난 후에 

CA 주소가 나오게 되면 해당 주소에 대한 값을 가지고 크기 비교를 시작한다.

 

그리고 주소가 존재하는 지 확인 한 후에

만약 Pair가 존재한다면 0인 주소가 안나와서 에러가 나고 0인 주소가 나오면 (Pair가 존재하지 않는다면) 에러가 난다.

그리고 어셈블리어로 create2를 사용한다. (이걸 사용하면 내부적으로 동일한 salt값을 사용하기 때문에 CA 주소를 미리 예측해서 테스트 해볼 수 있는 장점이 있다고 한다.)

 

여기서 사용한 salt는 token0, token1 값이다. 그냥 해시의 중복을 막기 위한 랜덤 값이라고 보면된다.

테스트 네트워크에서 해보면 실제랑 같은 주소가 나오니까 같은 환경에서 테스트하기에도 좋은것 같다.

 

그리고 바이트코드에는 미리 UniswapV2Pair의 creationCode를 사용하였는데 이부분은 처음 보는 부분이였다.

https://medium.com/authereum/bytecode-and-init-code-and-runtime-code-oh-my-7bcd89065904

 

Bytecode and Init Code and Runtime Code, Oh My!

tl;dr — There are only two types of bytecode on Ethereum but five different names to describe them.

medium.com

추가적으로 살펴보니

type(ContractName).creationCode

해당 코드대로 사용하면 바이트코드가 추출 될 수 있다고 한다. 정확히 말하면 constructo 로직이 포함된 부분의 바이트코드를 의미하므로 생성자 부분까지 들어간 형태의 바이트 코드를 도출할 때 사용되는것 같다.

 

만약 컨트렉트 생성이 아니라면 생성자 부분에 대한 변수나 함수는 빠진 상태로 솔리디티에서 도출 될 수 있는데 그 부분은

type(ContractName).runtimeCode

이렇게 runtimeCode라는 이름으로 정의하여 구할 수 있다.

 

        bytes memory bytecode = type(UniswapV2Pair).creationCode;

그래서 이런식으로 먼저 정의를 해 준 후에

        assembly {
            pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
        }

이런식으로 해서  create2를 실행시킨다.

https://docs.openzeppelin.com/cli/2.8/deploying-with-create2

 

Deploying Smart Contracts Using <code>CREATE2</code> - OpenZeppelin Docs

Under normal circumstances, sending funds to a random Ethereum address is a bad idea. Here however, we know we’ll be able to deploy Vault at the computed address and retrieve our funds. So let’s do it! The easiest way to send Ether is by using oz trans

docs.openzeppelin.com

create 2 는 selfdestruct가 있을 시 공격이 가능하다고 하니 이 부분은 넣으면 안된다고 한다.

new_address = hash(0xFF, sender, salt, bytecode)

여기서 0xff는 create와의 충돌을 막기 위함

두번째는 보내는 사람의 주소

그다음 salt, bytecode가 들어간다.

https://docs.soliditylang.org/en/v0.8.17/yul.html

 

*create2의 두번째 부분은 메타데이터가 들어가는 것이라 생략된다고 한다.


getPair 부분을 통하여서 각 페어의 값을 가져온 후에 이를 allPairs에 Push 해주면 양쪽 페어에 해당하는 LP토큰이 추가되게 된다.

728x90
반응형
Comments