티스토리 뷰
⛓️ 시리즈
2024.01.30 - [Solidity/DeFi] - [Uniswap] V2 Core - UniswapV2ERC20
2024.01.31 - [Solidity/DeFi] - [Uniswap] V2 Core - UniswapV2Factory
2024.01.31 - [Solidity/DeFi] - [Uniswap] V2 Core - UniswapV2Pair
🔢112 부호가 없는 고정 소수점 타입을 선택한 이유
추적된 토큰 보유량은 각각 112비트로 저장된다. 이는 스토리지 슬롯의 224비트를 사용하고 32비트가 남는다는 뜻인데, 이 남는 공간에 32비트 타임스탬프 저장함으로써 스토리지 비용을 절약할 수 있다.
그런데, 누적된 가격의 합은 224비트로는 부족할 수 있다. 따라서 256비트 크기로 스토리지에 저장되며 상위 32비트는 오버플로우 비트를 저장하기 위해 사용된다. 이로 인해 블록의 첫 번째 거래에서 SSOTRE 작업을 3회에 걸쳐(스토리지 슬롯 3개를 업데이트해야 하므로) 실행해야 한다. (가스비가 늘어나지만 어쩔 수 없는 부분)
32비트 타임스탬프도 향후 오버플로우가 발생할 수 있다. 그 시점은 2106년 2월 7일 경이될 것이다. 마찬가지로 해당 시점을 기준으로 232 - 1 초가 지날 때마다 오버플로우가 발생한다. (Uniswap V2는 solidity 0.5.0으로 작성되었기 때문에 오버플로우가 발생할 수 있고 허용된다. 그러나 최신 버전인 0.8.x로 작성한다면 timeElapsed를 계산하는 과정에서 발생한 오버플로우로 인해 전체 트랜잭션이 revert 된다!)
물론 타임스탬프 값이 32비트를 넘어가더라도 누적값 계산은 block.timestamp를 232로 나눈 나머지를 사용하므로 오버플로우가 발생하지 않는다. 가격 오라클을 사용하는 경우는 이 점을 유의하자. (그러나 2106년까지 Uniswap V2가 살아있을 것 같지는 않다...)
💸 프로토콜 수수료
Uniswap V2는 0.05%의 프로토콜 수수료를 청구하는데 이는 factory 컨트랙트의 feeTo 주소를 설정하여 껐다 켰다 할 수 있다.
수수료가 활성화되어 있으면 유동성 제공자들에게 돌아가는 0.3%의 수수료의 1/6을 프로토콜 수수료로 떼간다. 즉, 거래자들은 매 거래마다 0.3%의 수수료를 지불하는데, 이중 83.3%(거래액의 0.25%)는 유동성 제공자에게 돌아가고, 16.6%(거래액의 0.05%)는 프로토콜 수수료로써 feeTo 주소로 넘어간다.
수수료를 매 거래마다 계산하고 발행하는 것은 추가적인 가스비가 포함되므로 거래자들에게 부담이 될 수 있다. 따라서 누적된 수수료는 유동성이 예치되거나 인출되기 직전에 발행된다.
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); // 수수료 발행
...
emit Mint(msg.sender, amount0, amount1);
}
풀에 누적된 수수료는 지난번 수수료를 계산했을 때를 기점으로 √k의 성장률을 통해 계산된다. 지난번의 수수료 발행 시점을 t1 그리고 새로 수수료를 발행하고자 하는 현시점을 t2라고 했을 때, t1에서 수수료를 발행하고 난 뒤의 k (x * y)를 k1, t2에서 수수료를 발행하기 전의 k를 k2라고 했을 때, t1과 t2 사이의 누적된 수수료를 t2 시점에 유동성 풀에서의 유동성 비율로 계산하는 공식은 다음과 같다.
만약 수수료가 t1 이전에 활성화되어 있었다면 feeTo 주소는 t1과 t2 사이의 누적된 수수료의 1/6을 가져간다. 따라서 수수료로 feeTo에게 발행되는 유동성 토큰의 비율은 다음과 같다. (φ = 1/6)
(5) 공식을 (4)의 공식을 대입하여 sm에 대해 정리하여 재작성하면 다음과 같다. 여기서 s1은 t1 시점에 발행되어 있는 유동성 토큰의 총량(totalSupply)이며, sm은 프로토콜 수수료로 발행될 유동성 토큰의 양이다.
φ에 1/6을 대입하면 다음과 같다.
초기 유동성 공급자 Alice가 100 DAI와 1 ETH를 페어로 공급하고 10개의 유동성 토큰을 받았다고 가정해 보자. 몇 차례의 스왑이 발행한 뒤에 96 DAI와 1.5 ETH가 유동성 풀에 들어있는 상태에서 Alice가 자신이 유치한 자금을 인출하고자 한다. 이때 계산되는 프로토콜 수수료 sm은 다음과 같다.
💱 거래 수수료 납부 여부 확인
Uniswap V1에서는 다음 공식을 사용하여 단순히 하나의 토큰에서 다른 토큰으로 교환되는 경우만 따질 수 있었다. x를 y로 스왑 할 때는 입력된 x의 0.3%만큼의 토큰이 더 들어왔는지만 확인했고, y를 x로 스왑 할 때는 입력된 y의 0.3%만큼의 토큰이 더 들어왔는지만 확인했다.
Uniswap V2에서는 플래시 스왑이 도입됨에 따라 x의 입력량과 y의 입력량이 모두 0이 아닐 가능성이 존재하므로 다음 공식을 사용하여 수수료가 납부되었는지 확인한다.
이를 온체인 상에서 더 간단하게 계산하기 위해, 양 변에 1000000을 곱하여 계산한다.
{
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"
);
}
🪙 유동성 토큰의 공급에 대해
새로운 유동성 제공자가 유동성 풀에 토큰을 예치할 때, 발행되는 유동성 토큰의 양은 다음과 같이 계산된다.
liquidity = Math.min(
amount0.mul(_totalSupply) / _reserve0,
amount1.mul(_totalSupply) / _reserve1
);
만약 유동성 풀에 처음으로 유동성이 예치되는 상황(xstarting이 0인 경우)이라면, 이 공식은 동작하지 않을 것이다. (divide by 0)
Uniswap V1의 경우는 ETH - ERC20 페어 풀에 최초로 유동성을 예치할 때, 예치된 ETH의 양(wei 단위)과 동일한 유동성 토큰을 발행하였다. 만약 올바른 가격으로 ETH - ERC20 페어가 예치되었다면, 1 유동성 토큰의 가치는 대략 2 ETH와 동등할 것이기 때문이다. 그러나 이 방식은 유동성 토큰의 가치가 최초의 유동성 제공(임의적이고 실제 가격이 반영되었음이 보장되지 않는) 비율(ETH : ERC20)에 따라 달라짐 의미한다. 게다가, Uniswap V2는 ETH를 전혀 포함하지 않는 임의의 ERC20 토큰 쌍을 지원한다.
Uniswap V2는 V1에서 사용했던 방식을 사용하는 대신, 최초로 예치된 유동성의 기하평균과 동일한 양의 유동성 토큰을 발행한다.
if (_totalSupply == 0) {
liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
_mint(address(0), MINIMUM_LIQUIDITY);
}
이 공식은 유동성 토큰의 가치가 유동성이 최초로 예치된 비율로부터 독립적임을 보장한다. 예를 들어, 1 ABC의 가격이 100 XYZ라고 가정해 보자. 유동성 풀에 최초로 2 ABC와 200 XYZ를 예치했다고 치면 유동성 제공자는 √2*200 = 20 유동성 토큰을 받게 된다. 그리고 해당 유동성 토큰의 가치는 시간이 지나더라도 여전히 2 ABC와 200 XYZ의 가치가 있으며, 누적된 수수료도 포함된다.
또한 기하평균의 비트수는 자산 X의 보유량의 비트수와 자산 Y의 보유량의 비트수를 합한 평균이 되므로 반올림 오류가 발생할 가능성이 줄어든다.
그러나 시간이 지남에 따라 거래 수수료가 누적되고 기부(유동성 풀에 자산을 주입하는 행위)를 통해 유동성 풀에 유치된 자산의 규모가 늘어남으로 인해 유동성 토큰의 가치가 커질 수 있다. 이론적으로 이런 식으로 유동성 토큰의 가치가 계속 커지다 보면 결국 최소 수량의 토큰(10-18)의 가치가 너무 커져서 소규모 유동성 제공자들이 유동성을 제공하는 것이 불가능해질 수 있다.
이를 완화하기 위해 Uniswap V2는 최초에 발행되는 유동성 토큰에서 10-15 (10-18 * 1000)를 제외하여 0x00 주소로 보낸다. (totalSupply는 유지된다.) 즉, 최초의 유동성 제공자는 √x*y - 103만큼의 유동성 토큰을 받게 된다.
uint public constant MINIMUM_LIQUIDITY = 10 ** 3;
_mint(address(0), MINIMUM_LIQUIDITY);
이 비용은 거의 모든 토큰 쌍에서는 무시될만한 비용이어야 하지만, 위의 방식으로 공격을 실행하고자 하는 공격자에게는 극적인 비용 증가를 불러일으킨다.
예를 들어, 유동성 토큰의 가치를 100달러로 올리려면 공격자는 10만 달러를 유동성 풀에 '기부'해야 한다. 그리고 이렇게 기부된 자금은 다시는 되찾을 수 없다.
📖 참고
https://uniswap.org/whitepaper.pdf
'Solidity > DeFi' 카테고리의 다른 글
[Uniswap] V2 Oracle 예제 (0) | 2024.02.21 |
---|---|
[Uniswap] V2 Router (0) | 2024.02.20 |
[Uniswap] V2 Core - UniswapV2Pair (0) | 2024.01.31 |
[Uniswap] V2 Core - UniswapV2Factory (1) | 2024.01.31 |
[Uniswap] V2 Core - UniswapV2ERC20 (1) | 2024.01.30 |