[Ethereum] Uniswap V2에 대해 알아보자
Uniswap
개념
uniswap은 liquidity pool 프로토콜로
x*y=k 공식을 사용해서 2개의 ERC-20 토큰 간 교환비율을 자동으로 조정한다.
liquidity pool에 자산을 예치하면 LP 토큰을 받게 되는데
LP 토큰을 사용해서 기존 자산을 상환받을 수 있다.

Oracle
oracle은 가격 정보를 보기 위해 사용하는 툴을 말한다.
대량의 자산을 넣어서 순간적으로 가격을 조작하는 것을 막기 위해
일정 시간동의 평균 가격인 TWAP (Time Weight Average Price)을 사용한다.


Contract
UniswapV2ERC20
ERC-20과 비슷한데 EIP-2612에서 정의된 permit 함수가 추가되었다.
오프체인 서명을 통해 approve를 할 수 있게 해서 approve와 transfer를 한 번의 트랜잭션으로 할 수 있게 해준다.
아래에서 EIP-721 서명을 통해 owner인지 검증하고 _approve 함수를 호출한다.
function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
bytes32 digest = keccak256(
abi.encodePacked(
'\x19\x01',
DOMAIN_SEPARATOR,
keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
)
);
address recoveredAddress = ecrecover(digest, v, r, s);
require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
_approve(owner, spender, value);
}
UniswapV2Pair
contract를 생성해주는 factory와 2개의 ERC-20 주소를 저장하고 있다.
address public factory;
address public token0;
address public token1;
각 토큰의 잔고와
잔고가 업데이트 되는 시간을 저장하고 있다.
uint112 private reserve0;
uint112 private reserve1;
uint32 private blockTimestampLast;
TWAP를 구하기 위해 사용되는 변수와
x*y=k에서 사용되는 k를 저장한다.
uint public price0CumulativeLast;
uint public price1CumulativeLast;
uint public kLast;
reentrance 공격을 막기위한 modifier이다.
uint private unlocked = 1;
modifier lock() {
require(unlocked == 1, 'UniswapV2: LOCKED');
unlocked = 0;
_;
unlocked = 1;
}
아래는 업데이트 함수로 여러 변수를 업데이트 해주고 있다.
function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');
uint32 blockTimestamp = uint32(block.timestamp % 2**32);
uint32 timeElapsed = blockTimestamp - blockTimestampLast;
if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
}
reserve0 = uint112(balance0);
reserve1 = uint112(balance1);
blockTimestampLast = blockTimestamp;
emit Sync(reserve0, reserve1);
}
아래는 공급자에게 LP 토큰을 발행해주는 로직이다.
1. 새로 예치된 자산을 통해 liquidity를 계산하고 _mint 함수를 호출해서 LP 토큰을 발급한다.
처음에는 MINIMUM_LIQUIDITY만큼을 0x0으로 발행한다.
2. _update 함수를 호출한다.
3. _mintFee 함수를 호출해서 수수료가 있다면 kLast를 업데이트한다.
function mint(address to) external lock returns (uint liquidity) {
(uint112 _reserve0, uint112 _reserve1,) = getReserves();
uint balance0 = IERC20(token0).balanceOf(address(this));
uint balance1 = IERC20(token1).balanceOf(address(this));
uint amount0 = balance0.sub(_reserve0);
uint amount1 = balance1.sub(_reserve1);
bool feeOn = _mintFee(_reserve0, _reserve1);
uint _totalSupply = totalSupply;
if (_totalSupply == 0) {
liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
_mint(address(0), MINIMUM_LIQUIDITY);
} else {
liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
}
require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
_mint(to, liquidity);
_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1);
emit Mint(msg.sender, amount0, amount1);
}
아래는 토큰을 교환하는 로직이다.
1. _safeTransfer 함수를 호출해서 토큰을 전송한다.
data가 있으면 flash swap 로직을 수행한다.
2. 입금된 자산이 있는지 검증한다.
3. 입금 자산에 0.3%의 수수료를 부과하고 잔고가 감당가능한지 검증한다.
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
(uint112 _reserve0, uint112 _reserve1,) = getReserves();
require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
uint balance0;
uint balance1;
{
address _token0 = token0;
address _token1 = token1;
require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out);
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out);
if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
}
uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
{
uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
}
_update(balance0, balance1, _reserve0, _reserve1);
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}