유니스왑V3 2번째- initialize pool + FeeProtocol

Justin Gee
7 min readMar 19, 2023

--

Uniswap Labs, 트위터 캡처화면

유니스왑V3 1번째 — deploy pool
유니스왑V3 2번째 — initialize pool + FeeProtocol

유니스왑V3 1번째에 이어서 2번째(deploy pool)을 해볼 것이다.

initialize하기 위해서 먼저 v3-core/contracts/UniswapV3Pool.sol의 initialize function을 보자.

여기 파라미터를 보면 sqrtPriceX96이다. 유니스왑은 Q64.96포맷을 사용해서 Price를 저장한다. 정수 부분은 64비트, 분수 부분은 96비트로 구성된 이진 고정 소수점 숫자다. 그래서 sqrt(Price)에 2⁹⁶을 곱하면 된다. (Precision을 위해서)

6 : slot0에 저장하기 위해 tick을 계산한다. tick은 log_1.0001(Price)로 계산한다.​

파라미터와 slot0을 보면 알겠지만, token0의 리저브와 token1의 리저브를 저장하지 않는다. sqrtPriceX96과 tick만을 저장하고 있다. UniswapV3에서 스왑할 때 token의 리저브를 통해서 price를 계산하지 않는다. 나중에 유동성을 추가할텐데 그때 liquidity변수가 있다. 스왑할 inputAmount, liquidity와 sqrtPriceX96으로 outputAmount가 바로 계산되기 때문에 token의 리저브를 저장하지 않는다.

13~15 : Observations는 유니스왑 Price 오라클 관련 변수들인데 아직 다루지 않을 것이다. 나중에 오라클만 따로 작성할 것이다.

17 : unlocked는 swap할 때 re-entrancy를 막기 위해서 사용된다. 학부(컴퓨터과학부) 때 운영체제 수업을 들은 적이 있는데 그때 나온 mutex 패턴과 똑같다. race condition을 예방하기 위한 상호배제 속성이다.

16 : feeProtocol은 스왑시에 유니스왑 팀원들이 가져갈 수 있는 fee다. fee는 어떻게 결정될까? setFeeProtocol을 보자.

파라미터를 보면, feeProtocol0과 feeProtocol1이 있다. 즉 token0과 token1에 다른 fee를 매길 수 있다.

6~9 : require를 보면 feeProtocol은 4이상 10이하다. 이것은 스왑 수수료의 1/4 혹은 1/10까지만 설정이 가능한 것이다.

11 : token0과 token1에 따로 수수료를 매길 수 있다고 했는데 한번에 slot0.feeProtocol에 저장한다. 이게 무슨 일인가? 그것도 feeProtocol1에는 4만큼 left Shift(곱하기 16)하고 둘을 더해서 저장한다. 아래 이벤트를 보면 된다.

12

  • feeProtocol0은 feeProtocol % 16을 통해서 구할 수 있다. feeProtocol0 + (feeProtocol1 << 4)에서 feeProtocol1<<4 % 16을 하면 0이 된다. 16배수이기 때문이다. 그리고 feeProtocol0은 10이하기 때문에 값이 그대로 나온다.
  • feeProtocol2는 feeProtocol>>4를 통해서 구할 수 있다. 왜냐하면 feeProtcol0은 10 이하이기 때문에 소숫점으로 버려진다.
  • 왜 이렇게 했을까? 가스를 아끼기 위함일까. https://github.com/crytic/evm-opcodes 링크를 보면 각 Opcode마다 gas 소비량을 알 수 있다. SSTORE을 보면 20000**만큼 SLOAD는 800만큼 가스가 소비된다. 반면에 shift는 3, modulo remainder는 5만큼 소비된다. 즉 SSTORE과 SLOAD를 두번씩하는 것보다, 한번하고 shift와 mudlo remainder를 하는 것이 훨씬 싸다.

자 이제 테스트를 해보겠다.

current price를 5000 USDC / 1 ETH로 설정할 것이다. 이 테스트케이스는 https://uniswapv3book.com/에서 가져왔다.

그런데 위에서 initialize는 파라미터로 sqrtPriceX96를 받는다. 이것을 계산하는 함수는 다행히 v3-core/test/shared/utilities.ts에 enocdePriceSqrt function으로 구현되어 있다. 나는 이것을 자바스크립트로 변환해서 사용했다.

const { ethers } = require("hardhat")
const bn = require("bignumber.js")
bn.config({ EXPONENTIAL_AT: 999999, DECIMAL_PLACES: 40 })
// returns the sqrt price as a 64x96
function encodePriceSqrt(reserve0, reserve1) {
return ethers.BigNumber.from(
new bn(reserve1.toString())
.div(reserve0.toString())
.sqrt()
.multipliedBy(new bn(2).pow(96))
.integerValue(3) //ROUND_FLOOR
.toString()
)
}
module.exports = { encodePriceSqrt }

보기 쉽게 수식으로 그리겠다. 한 줄로 요약하면 sqrt(usdc reserve/eth reserve) * 2⁹⁶ 이다.

테스트 코드는 너무 간단하다.

it("initializes pool correctly", async () => {
const price = encodePriceSqrt(1, 5000)
await pool.initialize(price)
const {
sqrtPriceX96,
tick,
observationIndex,
unlocked,
observationCardinality,
observationCardinalityNext,
unlock,
} = await pool.slot0()
assert.equal(unlocked, true)
assert.equal(tick, await tickMath.getTickAtSqrtRatio(price))
console.log(
tick,
sqrtPriceX96.toString(),
observationIndex,
observationCardinality,
observationCardinalityNext
)
})

여기서 tickMath.getTickAtSqrtRation는 v3-core/contracts/libraries/tickMath에 구현되어 있으며, v3-core/contracts/test/TickMathTest.sol 을 배포해서 테스트했다.

*이 테스트는 모든 경우와 코드를 커버하지 않는다. 단지 흐름을 파악하기 위함이다.

--

--