Uniswap Protocal

birdgang
25 min readJul 21, 2024

--

1. 유동성 풀 (Liquidity Pool)

Uniswap에서는 각 거래 페어(예: ETH/DAI)에 대해 하나의 유동성 풀이 존재합니다. 유동성 풀은 두 개의 토큰으로 구성되며, 이 두 토큰의 유동성을 유동성 제공자들이 공급합니다. 유동성 풀은 스마트 컨트랙트에 의해 관리됩니다.

2. AMM (Automated Market Maker)

Uniswap 은 전통적인 오더북 기반 거래소와 달리 자동화된 시장 조성자(AMM) 모델을 사용합니다. AMM은 사전에 정의된 수학적 공식에 따라 가격을 결정하며, 거래 상대방 없이도 토큰 거래를 가능하게 합니다.

3. 상수곱 공식 (Constant Product Formula)

Uniswap 의 핵심 원리는 상수곱 공식을 사용하는 것입니다. 이는 다음과 같이 표현됩니다.

x × y = k

x 와 y는 유동성 풀에 있는 두 토큰의 수량.
k 는 상수로, 거래 후에도 변하지 않습니다.

예를 들어, ETH 와 DAI 로 구성된 유동성 풀이 있다고 가정 합니다. 이 풀에서 ETH 를 구매하려면 DAI 를 예치해야 하며, 이 과정에서 풀의 ETH 와 DAI 의 양이 변경되지만, 두 수량의 곱은 항상 일정하게 유지 됩니다.

4. 유동성 제공자 (Liquidity Providers)

유동성 제공자는 두 개의 토큰을 유동성 풀에 예치함으로써 유동성을 공급합니다. 예를 들어, ETH/DAI 풀에 유동성을 공급하려면 ETH 와 DAI 를 동일한 비율로 예치 해야 합니다. 유동성 제공자는 그 대가로 유동성 토큰을 받으며, 이는 유동성 풀에 대한 지분을 나타냅니다.

5. 수수료 구조 (Fees)

Uniswap에서 발생하는 모든 거래는 0.3%의 수수료가 부과됩니다. 이 수수료는 유동성 풀에 직접 추가되며, 유동성 제공자들이 분배 받습니다. 이를 통해 유동성 제공자는 거래 수수료로부터 이익을 얻습니다.

6. 가격 결정 (Price Determination)

가격은 유동성 풀 내 두 토큰의 비율에 따라 결정됩니다. 예를 들어, ETH/DAI 풀에서 ETH 의 가격은 풀에 있는 DAI의 수량을 ETH의 수량으로 나눈 값으로 결정됩니다. 거래가 발생하면 이 비율이 변하고, 따라서 가격도 변하게 됩니다.


Uniswap의 동작 과정 예시


유동성 공급: 사용자가 ETH/DAI 풀에 10 ETH와 2000 DAI를 예치합니다.
- 풀의 초기 상태: 10 ETH, 2000 DAI
- 상수 k = 10 × 2000 = 20000

토큰 스왑: 다른 사용자가 1 ETH를 구매하고자 220 DAI를 예치 합니다.
- 예치 전 풀 상태: 10 ETH, 2000 DAI
- 예치 후 풀 상태: 10 - Δx ETH, 2000 + 220 DAI
- 새로운 상수곱 조건: (10 - \(\Delta x) \times (2000 + 220) = 20000)

스왑 결과 : Δx를 계산하여 새로운 풀 상태를 얻습니다.
- Δx≈0.978ETH
- 스왑 후 풀 상태: 10 - 0.978 = 9.022 ETH, 2000 + 220 = 2220 DAI

가격 변화: 거래 후 ETH 가격이 변경됩니다.
- 새 ETH 가격: 2220 / 9.022 ≈ 246.08 DAI

Uniswap V2는 주로 세 가지 주요 컨트랙트로 구성됩니다. UniswapV2Factory, UniswapV2Pair, UniswapV2Router02. 각 컨트랙트는 특정 기능을 담당하며, 함께 작동하여 탈중앙화 거래소(DEX)를 운영 합니다.

1. UniswapV2Factory

UniswapV2Factory 컨트랙트는 새로운 유동성 풀(pair)을 생성하고 관리하는 역할을 합니다.

pragma solidity =0.5.16;

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

contract UniswapV2Factory is IUniswapV2Factory {
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);

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

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

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

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;
}
}
  • createPair : 새로운 유동성 풀을 생성합니다.
  • setFeeTo : 수수료를 받을 주소를 설정합니다.
  • setFeeToSetter : 수수료 설정자의 주소를 변경합니다.

2. UniswapV2Pair

UniswapV2Pair 컨트랙트는 개별 유동성 풀을 관리하며, 토큰 스왑, 유동성 공급/제거 기능을 담당합니다.

pragma solidity =0.5.16;

import './interfaces/IUniswapV2Pair.sol';
import './UniswapV2ERC20.sol';
import './libraries/Math.sol';
import './libraries/UQ112x112.sol';
import './interfaces/IERC20.sol';
import './interfaces/IUniswapV2Factory.sol';
import './interfaces/IUniswapV2Callee.sol';

contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {
using SafeMath for uint;
using UQ112x112 for uint224;

uint public constant MINIMUM_LIQUIDITY = 10**3;
bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));

address public factory;
address public token0;
address public token1;

uint112 private reserve0; // uses single storage slot, accessible via getReserves
uint112 private reserve1; // uses single storage slot, accessible via getReserves
uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves

uint public price0CumulativeLast;
uint public price1CumulativeLast;
uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event

uint private unlocked = 1;
modifier lock() {
require(unlocked == 1, 'UniswapV2: LOCKED');
unlocked = 0;
_;
unlocked = 1;
}

function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
_reserve0 = reserve0;
_reserve1 = reserve1;
_blockTimestampLast = blockTimestampLast;
}

function _safeTransfer(address token, address to, uint value) private {
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));
require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');
}

event Mint(address indexed sender, uint amount0, uint amount1);
event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
event Swap(
address indexed sender,
uint amount0In,
uint amount1In,
uint amount0Out,
uint amount1Out,
address indexed to
);
event Sync(uint112 reserve0, uint112 reserve1);

constructor() public {
factory = msg.sender;
}

// called once by the factory at time of deployment
function initialize(address _token0, address _token1) external {
require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
token0 = _token0;
token1 = _token1;
}

..
}
  • initialize : 페어 초기화.
  • getReserves : 현재 유동성 풀의 상태(토큰 수량)를 반환.
  • _safeTransfer : 토큰 전송을 안전하게 수행합니다.
  • _update : 유동성 풀의 상태를 업데이트하고, 가격 누적 값을 업데이트합니다.
  • _mintFee : 수수료가 설정된 경우 유동성 풀의 수수료를 계산하고 유동성을 민팅합니다.
  • mint : 유동성을 풀에 추가하고 유동성 토큰을 발행합니다.
  • burn : 유동성을 제거하고 유동성 토큰을 소각합니다.
  • swap : 두 토큰 간의 스왑을 수행합니다.
  • skim : 풀의 잔고와 실제 유동성(reserve) 간의 차이를 조정합니다.
  • sync : 풀의 상태를 현재 잔고와 동기화합니다.

3. UniswapV2Router02

UniswapV2Router02 컨트랙트는 유동성 추가, 제거 및 토큰 스왑과 같은 주요 기능을 제공합니다.

pragma solidity =0.6.6;

import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';
import '@uniswap/lib/contracts/libraries/TransferHelper.sol';

import './interfaces/IUniswapV2Router02.sol';
import './libraries/UniswapV2Library.sol';
import './libraries/SafeMath.sol';
import './interfaces/IERC20.sol';
import './interfaces/IWETH.sol';

contract UniswapV2Router02 is IUniswapV2Router02 {
using SafeMath for uint;

address public immutable override factory;
address public immutable override WETH;

modifier ensure(uint deadline) {
require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');
_;
}

constructor(address _factory, address _WETH) public {
factory = _factory;
WETH = _WETH;
}

receive() external payable {
assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract
}

function addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
..
}


// **** REMOVE LIQUIDITY ****
function removeLiquidity(
address tokenA,
address tokenB,
uint liquidity,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
..
}


// **** SWAP ****
// requires the initial amount to have already been sent to the first pair
function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {
..
}

function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
..
}


..
}
  • addLiquidity : 유동성을 풀에 추가하고 유동성 토큰을 발행합니다.
  • removeLiquidity : 유동성을 제거하고 유동성 토큰을 소각하며, 반환된 토큰을 사용자에게 전송합니다.
  • swapExactTokensForTokens : 입력한 정확한 토큰 수량으로 다른 토큰을 스왑합니다.

컨트랙트 배포

//migrations/2_deploy_contracts.js

const UniswapV2Factory = artifacts.require("UniswapV2Factory");
const WETH = artifacts.require("WETH");
const UniswapV2Router02 = artifacts.require("UniswapV2Router02");

module.exports = async function(deployer, network, accounts) {
const feeToSetter = accounts[0];
await deployer.deploy(UniswapV2Factory, feeToSetter);
const factory = await UniswapV2Factory.deployed();

await deployer.deploy(WETH);
const weth = await WETH.deployed();

await deployer.deploy(UniswapV2Router02, factory.address, weth.address);
};
  • UniswapV2Factory를 배포하고, 수수료 설정 계정(feeToSetter)으로 초기화합니다.
  • WETH 컨트랙트를 배포합니다.
  • UniswapV2Router02 컨트랙트를 배포하고, UniswapV2FactoryWETH 컨트랙트 주소를 설정 합니다.

PAIR 생성

const UniswapV2Factory = artifacts.require("UniswapV2Factory");
const UniswapV2Router02 = artifacts.require("UniswapV2Router02");

module.exports = async function(callback) {
try {
const accounts = await web3.eth.getAccounts();
const factory = await UniswapV2Factory.deployed();
const router = await UniswapV2Router02.deployed();

await factory.createPair("0xTokenAAddress", "0xTokenBAddress", { from: accounts[0] });
console.log("Pair created!");

// Add liquidity, remove liquidity, and swap tokens...
} catch (error) {
console.error(error);
}
callback();
};
  • 사용 가능한 계정 목록을 가져옵니다.
  • 배포된 UniswapV2FactoryUniswapV2Router02 컨트랙트 인스턴스를 가져옵니다.
  • UniswapV2FactorycreatePair 함수를 호출하여 새로운 페어를 생성합니다.
  • 스크립트 실행이 끝나면 callback 함수를 호출하여 종료합니다.

유동성 추가

유동성을 추가하려면 두 개의 토큰을 미리 준비해야 합니다. 여기서는 가상의 토큰 TokenATokenB를 사용합니다.

// addLiquidity.js

const UniswapV2Router02 = artifacts.require("UniswapV2Router02");
const IERC20 = artifacts.require("IERC20");

module.exports = async function(callback) {
try {
const accounts = await web3.eth.getAccounts();
const router = await UniswapV2Router02.deployed();
const tokenA = await IERC20.at("0xTokenAAddress");
const tokenB = await IERC20.at("0xTokenBAddress");

const amountADesired = web3.utils.toWei('10', 'ether'); // 10 TokenA
const amountBDesired = web3.utils.toWei('20', 'ether'); // 20 TokenB
const amountAMin = web3.utils.toWei('9', 'ether'); // 최소 9 TokenA
const amountBMin = web3.utils.toWei('18', 'ether'); // 최소 18 TokenB

// 토큰 전송 승인
await tokenA.approve(router.address, amountADesired, { from: accounts[0] });
await tokenB.approve(router.address, amountBDesired, { from: accounts[0] });

// 유동성 추가
const tx = await router.addLiquidity(
tokenA.address,
tokenB.address,
amountADesired,
amountBDesired,
amountAMin,
amountBMin,
accounts[0],
Math.floor(Date.now() / 1000) + 60 * 10, // 10분 뒤 만료
{ from: accounts[0] }
);

console.log('Liquidity added:', tx);
} catch (error) {
console.error(error);
}
callback();
};
  • tokenA : 유동성을 추가할 첫 번째 토큰 주소.
  • tokenB : 유동성을 추가할 두 번째 토큰 주소.
  • amountADesired : 유동성으로 추가할 첫 번째 토큰의 양.
  • amountBDesired : 유동성으로 추가할 두 번째 토큰의 양.
  • amountAMin : 유동성으로 추가할 첫 번째 토큰의 최소 허용 양.
  • amountBMin : 유동성으로 추가할 두 번째 토큰의 최소 허용 양.
  • to : 유동성 토큰을 받을 주소.
  • deadline : 트랜잭션이 유효한 마지막 시간(UNIX 타임스탬프).

토큰 스왑

한 토큰을 다른 토큰으로 스왑하려면 swapExactTokensForTokens 함수를 사용합니다.

// swapTokens.js

const UniswapV2Router02 = artifacts.require("UniswapV2Router02");
const IERC20 = artifacts.require("IERC20");

module.exports = async function(callback) {
try {
const accounts = await web3.eth.getAccounts();
const router = await UniswapV2Router02.deployed();
const tokenA = await IERC20.at("0xTokenAAddress");
const tokenB = await IERC20.at("0xTokenBAddress");

const amountIn = web3.utils.toWei('1', 'ether'); // 1 TokenA
const amountOutMin = web3.utils.toWei('1', 'ether'); // 최소 1 TokenB

// 토큰 전송 승인
await tokenA.approve(router.address, amountIn, { from: accounts[0] });

// 경로 설정
const path = [tokenA.address, tokenB.address];

// 토큰 스왑
const tx = await router.swapExactTokensForTokens(
amountIn,
amountOutMin,
path,
accounts[0],
Math.floor(Date.now() / 1000) + 60 * 10, // 10분 뒤 만료
{ from: accounts[0] }
);

console.log('Tokens swapped:', tx);
} catch (error) {
console.error(error);
}
callback();
};
  • amountIn : 스왑할 토큰의 양.
  • amountOutMin : 스왑 후 받을 최소 토큰의 양.
  • path : 토큰 스왑 경로 (예: [tokenA, tokenB]).
  • to : 스왑 후 토큰을 받을 주소.
  • deadline : 트랜잭션이 유효한 마지막 시간(UNIX 타임스탬프).

페어 생성

  • 사용자(User)가 UniswapV2FactorycreatePair 함수를 호출하여 새로운 페어를 생성 합니다.
  • UniswapV2Factory는 페어가 이미 존재하는지 확인하고, 존재하지 않으면 새로운 UniswapV2Pair를 생성합니다.
  • 생성된 페어의 주소를 사용자에게 반환합니다.

유동성 추가

  • 사용자(User)가 TokenATokenB 컨트랙트에 Router를 통해 유동성을 추가할 수 있도록 승인(approve)합니다.
  • 사용자가 RouteraddLiquidity 함수를 호출하여 유동성을 추가 합니다.
  • RouterUniswapV2Factory를 통해 페어 주소를 확인하고, 해당 페어에 TokenATokenB를 전송 합니다.
  • 페어는 유동성을 추가하고, 유동성 토큰을 사용자 에게 발행 합니다.

토큰 스왑

  • 사용자(User)가 TokenA 컨트랙트에 Router를 통해 토큰을 스왑할 수 있도록 승인(approve)합니다.
  • 사용자가 RouterswapExactTokensForTokens 함수를 호출하여 TokenATokenB로 스왑 합니다.
  • RouterUniswapV2Factory를 통해 페어 주소를 확인하고, 해당 페어에 TokenA를 전송 합니다.
  • 페어는 TokenA를 수신하고, TokenB를 사용자에게 전송 합니다.

--

--