이 기사에서는 UniswapV4 기능의 의미와 구현 원리를 자세히 설명합니다.
원저자: Lin Weichen Albert Lin
원본출처 : 미디엄
UniswapV4 발표 이후 Swap 플랫폼은 큰 변화를 겪었습니다. Swap 플랫폼에서 인프라 서비스 제공자로 진화했습니다. 특히, V4의 Hooks 기능이 큰 주목을 받았습니다. 저는 심도 깊은 연구 기간을 거쳐 모든 사람이 이 변화와 그 구현에 대해 더 잘 이해할 수 있기를 바라면서 몇 가지 콘텐츠를 편집했습니다.
UniswapV4의 혁신 초점은 AMM 기술을 향상시키는 것이 아니라 생태계를 확장하는 것입니다. 특히 이 혁신에는 다음과 같은 주요 기능이 포함됩니다.
- 플래시 회계
- 싱글톤 계약
- 후크 아키텍처
다음 섹션에서는 이러한 기능이 무엇을 의미하고 어떻게 작동하는지 자세히 설명하겠습니다.

플래시 회계
복식부기
UniswapV4는 Double Entry Bookkeeping과 유사한 기록 방식을 채택하여 각 작업에 해당하는 토큰 잔액의 증가 또는 감소를 추적합니다. 이 복식 장부 방법에서는 각 거래가 동시에 여러 계정에 기록되어야 하며 이러한 계정 간의 자산 가치가 균형을 이루도록 해야 합니다. 예를 들어, 사용자가 100 TokenA를 풀에서 50 TokenB로 교환한다고 가정하면 원장의 기록은 다음과 같습니다.
- 사용자: TokenA는 100단위(-100) 감소하고 TokenB는 50단위(+50) 증가합니다.
- POOL: TokenA는 100단위(+100) 증가하고 TokenB는 50단위(-50) 감소합니다.

토큰 델타 관련 운영
UniswapV4에서는 이 회계 방법이 주요 작업에 사용되며 lockState.currencyDelta[currency]라는 저장 변수가 프로그램 코드에서 토큰 잔액의 변경 사항을 기록하는 데 사용됩니다. 이 변경 값이 양수이면 풀의 토큰 수가 예상되는 증가를 나타내고, 그렇지 않으면 풀의 토큰 수가 예상되는 감소를 나타냅니다. 다른 관점에서 볼 때, 값이 양수이면 풀에 누락된 토큰 수(수신할 것으로 예상되는 토큰 수)를 나타내고, 음수 값은 풀에 있는 초과 토큰 수(예상되는 토큰 수)를 나타냅니다. 사용자가 출금할 수 있음) 토큰 수량). 다음은 토큰 델타(TokenDelta)에 대한 다양한 작업의 효과를 나열합니다.

- 수정포지션: 유동성 추가/제거 작업을 나타냅니다. 유동성 추가의 경우 토큰 델타(풀에 추가될 것으로 예상되는 TokenA를 나타냄)는 추가를 통해 업데이트됩니다. 유동성 제거의 경우 빼기가 사용되어 토큰 델타를 업데이트합니다(TokenB가 풀에서 인출될 것으로 예상됨을 나타냄).
- swap: Swap 작업을 수행함을 나타냅니다. 예를 들어 TokenA를 TokenB로 교환하면 더하기는 TokenADelta를 업데이트하는 데 사용되고 빼기는 TokenBDelta를 업데이트하는 데 사용됩니다.
- 결제: 토큰을 풀로 전송하는 작업이 수반됩니다. 풀은 전후의 토큰 증가를 계산하고 빼기를 사용하여 TokenDelta를 업데이트합니다. 풀이 예상되는 수의 토큰을 수신하는 경우 여기서 빼기 업데이트는 TokenDelta를 0으로 재설정합니다.
- take: 풀에서 토큰을 인출하는 작업을 수반합니다. 풀은 추가 기능을 사용하여 TokenDelta를 업데이트하여 이 풀에서 토큰이 제거되었음을 나타냅니다.
- mint: TokenDelta 업데이트 동작은 mint가 실제로 풀에서 토큰을 인출하지 않는다는 점을 제외하면 "take"와 유사합니다. 대신, 출금 증명으로 해당 ERC1155 토큰이 발행되며, 토큰은 풀에 남아 있습니다. 이후 사용자는 ERC1155 토큰을 폐기하여 풀에서 토큰을 검색할 수 있습니다. 두 가지 목적이 있는 것으로 추측됩니다: 1. ERC20 토큰 전송의 가스 비용(계약 호출 + 스토리지 쓰기 1개 감소)을 절약하고 ERC1155 토큰 소각 방법을 사용하여 향후 거래 사용을 위해 TokenDelta를 업데이트합니다. 2. 사용자가 더 나은 스왑 토큰 경험을 가질 수 있도록 풀의 유동성을 유지하고 유동성 깊이를 유지하십시오.
- donate: 토큰을 풀에 기부하겠다고 선언하지만 실제로는 토큰을 풀에 보내려면 "settle"을 사용해야 합니다. 따라서 여기서는 추가를 사용하여 토큰 델타를 업데이트합니다.
위 작업 중 결제 및 가져오기 작업만 실제로 토큰을 전송하며, 다른 작업은 단순히 TokenDelta 값을 업데이트합니다.
토큰 델타 예시
아래에서는 TokenDelta를 실제로 업데이트하는 방법을 설명하기 위해 간단한 예를 사용합니다. 오늘 100 TokenA를 50 TokenB로 교환한다고 가정해 보겠습니다.

- TokenADelta와 TokenBDelta는 모두 트랜잭션이 시작되기 전에 0입니다.
- swap: 풀이 받아야 하는 TokenA 수와 사용자가 받게 될 TokenB 수를 계산합니다. 이때 TokenADelta = 100, TokenBDelta = -50입니다.
- 결제: 100 TokenA를 풀에 보내고 TokenADelta = 100–100 = 0으로 업데이트합니다.
- 예: 풀에서 사용자 계정으로 50 TokenB를 전송하고 TokenBDelta = -50 + 50 = 0을 업데이트합니다.
- 거래 후 TokenADelta와 TokenBDelta는 모두 0입니다.
전체 상환 작업이 완료되면 TokenADelta와 TokenBDelta는 모두 0으로 재설정됩니다. 이는 운영이 완전히 균형을 이루어 계정 잔액의 일관성을 보장한다는 것을 의미합니다.
EIP-1153: 임시 저장 opcode
앞서 언급했듯이 UniswapV4는 TokenDelta를 기록하기 위해 Storage Variable을 사용하지만 컨트랙트 내에서 Storage Variable을 읽고 쓰는 데에는 비용이 많이 듭니다. 이때 Uniswap이 출시한 또 다른 EIP인 EIP1153(Transient Storage Opcodes)에 대해 언급해야 합니다.
UniswapV4는 EIP1153에서 제공하는 두 개의 OP 코드 TSTORE 및 TLOAD를 사용하여 TokenDelta를 업데이트할 계획입니다. 임시 저장 Opcode를 사용하는 저장 변수는 Transaction 종료 후 폐기되므로(메모리 변수와 유사) 하드 디스크에 쓸 필요가 없으므로 가스 비용이 절감됩니다.
EIP1153은 다음 칸쿤 업그레이드에 포함될 것으로 확인됐고, UniswapV4도 칸쿤 업그레이드 이후에 UniswapV4가 출시될 것이라고 지적했다.

플래시 계정 - 잠금
UniswapV4에는 잠금 메커니즘이 도입되었습니다. 즉, 풀 작업을 수행하기 전에 먼저 PoolManager.lock()을 호출하여 잠금을 얻어야 합니다. lock() 실행이 끝나기 전에 TokenDelta 값이 0인지 확인하고, 그렇지 않으면 되돌리기가 트리거됩니다. PoolManager.lock()이 호출되어 잠금이 성공적으로 획득되면 msg.sender의 lockAcquired() 함수가 호출됩니다. 풀 관련 작업(예: swap, 수정Position 등)은 lockAcquired() 함수에서만 수행됩니다.
다음 다이어그램은 이 프로세스를 설명하기 위한 예로 사용됩니다. 사용자가 토큰 스왑 작업을 수행해야 하는 경우 lockAcquired() 함수가 포함된 스마트 계약을 호출해야 합니다(여기서는 콜백 계약, 콜백 계약이라고 함). 콜백 계약은 먼저 PoolManager.lock()을 호출한 다음 PoolManager가 콜백 계약의 lockAcquired() 함수를 호출합니다. lockAcquired() 함수에는 Swap, Set, Take 작업 등 Pool 작업과 관련된 로직이 정의되어 있습니다. 마지막으로 전체 lock()이 끝나려고 할 때 PoolManager는 이 작업과 관련된 TokenDelta가 0으로 재설정되었는지 확인하여 풀의 자산이 균형을 유지하는지 확인합니다.

싱글톤 계약
싱글톤 계약은 UniswapV4가 이전 Factory-Pool 모델을 포기했음을 의미합니다. 각 풀은 더 이상 독립적인 스마트 계약이 아니지만 모든 풀은 동일한 싱글톤 계약을 공유합니다. 이 설계는 Flash Accounting 메커니즘과 결합되어 필요한 스토리지 변수만 업데이트하면 되므로 운영의 복잡성과 비용이 더욱 줄어듭니다.
다음 다이어그램은 예시로 사용됩니다. UniswapV3를 예시로 사용하면 ETH를 DAI로 변환하려면 최소 4번의 토큰 전송(스토리지 쓰기 작업)이 필요합니다. 여기에는 USDC, USDT 및 DAI 토큰에 대한 여러 변경 기록이 포함됩니다. 그러나 UniswapV4 및 Flash Accounting 메커니즘의 개선을 통해 DAI를 풀에서 사용자에게 전송하기 위해 단 한 번의 토큰 전송만 필요하므로 운영 횟수와 비용이 크게 줄어듭니다.

후크 아키텍처
이번 UniswapV4 업데이트에서 가장 눈길을 끄는 것은 Hooks Architecture입니다. 이 업데이트는 풀 가용성에 대한 뛰어난 유연성을 제공합니다. Hooks는 Pool에서 특정 작업을 수행할 때 Hooks Contract가 추가로 호출되어 추가 작업을 수행한다는 의미입니다. 이러한 작업은 초기화(풀 생성), 위치 수정(유동성 추가/제거), 교환 및 기부를 포함하여 다양한 범주로 나눌 수 있습니다. 각 범주에는 사전 실행 및 사후 실행 작업이 있습니다.
- 초기화 전 / 초기화 후
- beforeModifyPosition / afterModifyPosition
- 스왑 전/스왑 후
- beforeDonate / afterDonate
이 설계를 통해 사용자는 특정 작업 전후에 맞춤형 로직을 보다 유연하게 실행할 수 있으므로 UniswapV4의 기능이 확장됩니다.

후크 예 - 지정가 주문 후크
다음으로 지정가 주문의 예를 사용하여 Hooks의 실제 작동 과정을 설명하겠습니다. 시작하기 전에 UniswapV4에서 지정가 주문을 구현하는 원리를 간략하게 설명하겠습니다.
UniswapV4 지정가 주문 메커니즘
UniswapV4에서 지정가 주문을 구현하는 원칙은 특정 가격 범위에 유동성을 추가(Add Liquidity)한 후 해당 범위의 유동성이 교환되면 Remove Liquidity 작업을 수행하는 것입니다.
예를 들어, 1900~2000 가격 범위에서 ETH에 유동성을 추가한 다음 ETH 가격이 1800에서 2100으로 상승할 때를 가정해 보겠습니다. 이때 이전에 1900~2000 가격대에 추가했던 모든 ETH 유동성이 USDC로 교환되었습니다(ETH-USDC 풀에 있다고 가정). 이 시점에서 유동성을 제거하면 현재 가격 1900-2000에서 ETH 시장 주문을 실행하는 효과가 있습니다.

제한 주문 후크 계약
이 예제는 UniswapV4의 GitHub에서 제공됩니다. 이 예에서 Limit Order Hook 계약은 afterInitialize와 afterSwap이라는 두 개의 후크를 제공합니다. 그 중 afterInitialize는 누군가가 스왑을 한 후 어떤 지정가 주문이 일치했는지 확인하기 위해 풀이 설정될 때 가격 범위(틱)를 기록하는 데 사용됩니다.
주문하기
사용자가 주문을 해야 하는 경우 Hook 계약은 사용자가 지정한 가격 범위와 수량에 따라 유동성을 추가하는 작업을 수행합니다. 지정가 주문의 Hook 컨트랙트에서 place() 함수를 볼 수 있습니다. 주요 로직은 잠금을 획득한 후 lockAcquiredPlace() 함수를 호출하여 유동성 추가 작업을 수행하는 것이며 이 부분은 지정가 주문을 하는 것과 같습니다.

afterSwap 후크
사용자가 이 풀에서 스왑 토큰을 완료한 후 풀은 Hook 계약의 afterSwap() 함수를 호출합니다. afterSwap의 주요 로직은 이전 가격 범위와 현재 가격 범위 사이에 체결된 주문에서 유동성을 제거하는 것입니다. 이러한 동작은 체결되는 주문과 동일합니다.

주문 흐름 제한
다음은 지정가 주문 프로세스의 개략도입니다.

- 주문자는 Hook 계약으로 주문을 보냅니다.
- Hook 계약은 주문 정보를 기반으로 유동성 추가 작업을 수행합니다.
- 일반적으로 사용자는 풀에서 토큰 교환 작업을 수행합니다.
- 스왑 토큰 작업이 완료된 후 풀은 Hook 계약의 afterSwap() 함수를 호출합니다.
- Hook 계약은 스왑 토큰 가격 범위의 변화에 따라 실행된 지정가 주문의 유동성 제거 작업을 수행합니다.
위는 Hook 메커니즘을 사용하여 Limit-Order를 구현하는 전체 과정입니다.
후크: 기타 기능
Hooks에는 저자가 연구 중에 흥미를 느꼈다고 생각한 몇 가지 흥미로운 사항이 있으며, 이는 모든 사람과 언급하고 공유할 가치가 있다고 생각합니다.
Hooks 계약 주소 비트
특정 이전/이후 작업을 수행해야 하는지 여부를 결정하는 것은 Hook 계약 주소의 가장 왼쪽 바이트에 의해 결정됩니다. 1바이트는 8비트와 동일하며 이는 정확히 8개의 추가 작업에 해당합니다. 풀은 Hook 계약의 해당 Hook 함수를 호출해야 하는지 여부를 결정하기 위해 작업 비트가 1인지 확인합니다. 이는 또한 Hook 계약의 주소를 특정 방식으로 설계해야 하며 계약 주소를 Hook 계약으로 임의로 선택할 수 없음을 의미합니다. 이 설계의 주요 목적은 가스 소비를 줄이고 비용을 계약 배포로 이전하여 보다 효율적인 운영을 달성하는 것입니다. (PS: 실제로는 다양한 CREATE2 솔트를 사용하여 적격 계약 주소를 무차별적으로 계산할 수 있습니다.)

동적 수수료
Hooks는 각 작업 전후에 추가 작업을 수행할 수 있을 뿐만 아니라 동적 수수료 구현도 지원합니다. 풀을 생성할 때 동적 처리 수수료 활성화 여부를 지정할 수 있습니다. 동적 수수료가 활성화된 경우 토큰 스왑 시 Hook 계약의 getFee() 함수가 호출됩니다. Hook 계약은 현재 Pool 상태에 따라 얼마만큼의 처리 수수료를 부과할지 결정할 수 있습니다. 이 설계를 통해 실제 상황에 따라 처리 수수료 계산을 조정할 수 있어 시스템의 유연성이 향상됩니다.
풀 생성
각 풀은 생성 시 Hook 계약을 결정해야 하며 나중에 변경할 수 없습니다(그러나 다른 풀은 동일한 Hook 계약을 공유할 수 있습니다). 이는 주로 후크가 작업을 수행할 풀을 식별하기 위해 PoolManager가 사용하는 PoolKey의 일부로 간주되기 때문입니다. 자산이 동일하더라도 Hook 계약이 다른 경우에는 다른 Pool로 간주됩니다. 이 설계를 통해 서로 다른 풀의 상태와 운영을 독립적으로 관리할 수 있고 풀 일관성이 보장됩니다. 그러나 동시에 풀 수가 증가함에 따라 라우팅의 복잡성도 증가합니다. 아마도 UniswapX는 이 문제를 해결하기 위해 설계된 방법 중 하나일 것입니다.

TL;DR
- 플래시 회계는 각 토큰 수량의 변경 사항을 추적하고 거래가 완료된 후 모든 변경 사항이 0으로 재설정되도록 하는 데 사용됩니다. Flash Accounting은 Gas 비용을 절약하기 위해 EIP1153에서 제공하는 특별한 저장 방식을 사용합니다.
- 싱글톤 계약의 설계는 여러 저장된 변수에 대한 업데이트를 방지하므로 가스 소비를 줄이는 데 도움이 됩니다.
- Hooks 아키텍처는 "사전 실행" 및 "사후 실행" 단계로 구분된 추가 작업을 제공합니다. 이는 각 풀 작업을 더욱 유연하게 만들지만 풀 라우팅을 더욱 복잡하게 만듭니다.
UniswapV4는 Uniswap 풀을 기반으로 더 많은 서비스가 구축될 수 있도록 전체 Uniswap 생태계를 확장하고 인프라로 구축하는 데 더 중점을 두고 있습니다. 이는 Uniswap의 경쟁력을 강화하고 다른 서비스로 대체될 위험을 줄이는 데 도움이 될 것이지만 예상만큼 성공할 수 있는지 여부는 추가 관찰이 필요합니다. 주요 내용 중 일부에는 Flash Accounting과 EIP1153의 결합이 포함되며, 앞으로 더 많은 서비스가 이러한 기능을 채택하고 다양한 애플리케이션 시나리오가 등장할 것이라고 믿습니다. 이것이 UniswapV4의 핵심 개념이며, 이를 통해 모든 사람이 UniswapV4의 작동 방식을 더 깊이 이해할 수 있기를 바랍니다. 기사에 오류가 있으면 바로잡아주시고, 토론하고 의견을 교환하는 것도 환영합니다.
마지막으로, 기사를 검토하고 귀중한 의견을 제시하는 데 도움을 준 Anton Cheng과 Ping Chen에게 감사드립니다!


