블록체인

Uniswap v2 핵심원리

Snag 2022. 1. 22. 23:05

유니스왑 V2 관계구성도

유니토큰 구조

유니스왑은 토큰을 표현하는 가장 기본적인 단위로 ERC20를 사용한다. 하지만 그대로 사용하는 것이 아니라 확장된 ERC20를 만들었는데, 그것이 IUniswapV2ERC20 인터페이스이다. 이 인터페이스는 ERC20의 기본 함수 이외에 아래와 같은 함수를 더 제공한다.

function DOMAIN_SEPARATOR() external view returns (bytes32);

function PERMIT_TYPEHASH() external pure returns (bytes32);

function nonces(address owner) external view returns (uint);

function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external;

ERC20 토큰은 사용하기 전에 approve()라는 함수를 호출하여 누가 어떤 컨트랙트에서 어느 정도의 이체한도를 쓸 것인지 정해야 한다. 현실 세계에서의 은행계좌도 보안상 이체 한도를 정하게 되는데, Defi에도 비슷한 장치를 구현해 놓았다.

하지만 이게 불편한 것은 처음 사용하는 토큰에 대해서는 지갑이 팝업으로 떠서 한 번은 승인을 반드시 해야 하고, 컨트랙트 실행에 대한 네트워크 수수료도 조금 내야 한다. 이체 한도는 기본적으로 최대한도가 설정된다. 최대한도는 uint의 최댓값이다.

LP토큰

유니스왑에 두 개의 토큰을 묶어서 유동성을 제공하게 되면, Liquidty Provider 토큰(이하 LP토큰)을 받는다. 이 LP토큰도 ERC20 규격이므로, 전송할 때 최대한도가 설정이 필요하다. 하지만, 토큰을 받을 때는 한도 설정이 필요 없고 보낼 때는 반드시 필요하다.

유동성을 제공하면 LP토큰을 받게 되므로, approve가 필요 없고, 유동성을 제거할 때 LP토큰을 컨트랙트 주소로 보내어 소각해야 하므로, 전송이 필요하다. 전송권한을 얻기 위해서는 유동성 제거 시에 approve()의 함수 호출이 필요하다.

그런데, approve를 별도로 호출하면 web3 지갑이 한번 뜨게 되므로, 사용자 경험에 좋지 않다. 그래서 유동성 제거시 web3 지갑에서 네트워크 수수료 없이 서명만 하면, 유동성을 제거하면서 approve에도 동시에 사용된다. 허용을 위한 함수가 permit이고, DOMAIN_SEPARATOR와 PERMIT_TYPEHASH도 서명 데이터를 만들기 위해 함께 존재한다.

 

LP토큰 인터페이스

interface IUniswapV2Pair {
    function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
    function price0CumulativeLast() external view returns (uint);
    function price1CumulativeLast() external view returns (uint);
    function kLast() external view returns (uint);

    function mint(address to) external returns (uint liquidity);
    function burn(address to) external returns (uint amount0, uint amount1);
    function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
    function skim(address to) external;
    function sync() external;
}

UniswapV2Pair는 IUniswapV2ERC20의 함수를 기본적으로 가지고 있으면서 위의 함수들을 추가적으로 구현해 놓았다. getReserves() 함수는 이 토큰 Pair 풀에 들어있는 각 토큰의 개수를 반환한다. mint, burn, swap은 페어 토큰을 발행하고, 소각하고 스왑 하는 기능을 한다.

 

풀 생성

contract UniswapV2Factory is IUniswapV2Factory {

	mapping(address => mapping(address => address)) public getPair;
    
	function createPair(address tokenA, address tokenB) external returns (address pair) {
		
            // 페어의 바이트코드
            bytes memory bytecode = type(UniswapV2Pair).creationCode;

            // 토큰 페어로 salt를 만든다.
            bytes32 salt = keccak256(abi.encodePacked(token0, token1));

            // 페어 컨트랙트를 인스턴스화 한다.
            assembly {
                pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
            }

            IUniswapV2Pair(pair).initialize(token0, token1);
        }
}

UniswapV2Factory는 유동성 Pool을 실제로 만들어준다. 토큰 페어 주소를 전달하면, 풀이 생성되어 getPair 상태 변수로 들어간다. 여기서는 인라인 어셈블리를 사용하여 create2로 컨트랙트를 생성한다. 페어에 대해서 풀은 하나만 존재해야 하므로, 토큰 이름 쌍으로 salt를 삼아 중복 풀이 만들어지는 것을 방지한다. 

 

유동성 추가

유니스왑 v2 유동성 추가화면

// 유동성 추가
function addLiquidity(
    address tokenA,
    address tokenB,
    uint amountADesired,
    uint amountBDesired,
    uint amountAMin,
    uint amountBMin,
    address to,
    uint deadline
) external override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
	
	//실제 토큰 갯수를 정확히 계산한다.
    (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
    address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
    // 토큰을 풀 컨트랙트로 전송한다.
    TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
    TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
    // LP토큰을 발행한다.
    liquidity = IUniswapV2Pair(pair).mint(to);
}

UniswapV2Router02 는 유동성을 추가/제거하는 기능을 가지고 있다. UniswapV2Router는 01과 02가 있는데, 01은 보안상 문제가 있어서 02를 새로 만들었다. 01은 사용하면 안된다. addLiquidity는 두 개의 토큰 주소와 두 토큰의 개수를 입력하면 _addLiquidity 함수로 토큰 간의 비율을 계산해서 정확한 개수로 풀에 토큰을 전송한다. 토큰 전송이 성공하면 LP토큰을 발급받게 된다.

 

유동성 제거

function removeLiquidity(
    address tokenA,
    address tokenB,
    uint liquidity,
    uint amountAMin,
    uint amountBMin,
    address to,
    uint deadline
) public override ensure(deadline) returns (uint amountA, uint amountB) {
    address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
    // LP토큰을 풀 컨트랙트에 다시 넣는다.
    IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair
    // LP토큰을 소각한다.
    (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
    (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
    (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
    require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
    require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
}

유동성 제거는 removeLiquidity를 통해서 진행된다. 제거하는 과정은 개인 지갑에 가지고 있던 LP토큰이 풀 컨트랙트로 전송되고 소각된다. 그리고 풀에 들어있던 두 토큰이 내 개인 지갑으로 들어온다. 

'블록체인' 카테고리의 다른 글

알케미(Alchemy)  (0) 2022.02.11
DeFi 의 높은 이자는 어디서 나올까?  (0) 2022.02.10
솔리디티 바이트코드  (0) 2022.01.22
테라 백서 한글번역  (0) 2022.01.18
이더리움 도메인 하나 등록하는데, 27만원!?  (0) 2021.03.30