
개발자 노트: Aggregator 훅의 탄생
글: Attens & Bruce
편집: Lisa, DODO Research
이것(Aggregator Hook)은 다른 DEX들에게도 좋은 일이다. 코드 한 줄만 추가하면 '슈퍼 집계기'에 하나 더 연결할 수 있으니, 왜 하지 않을까? 당연히 유니스왑(Uniswap)에게도 좋은 일이다. 타 DEX의 유동성을 매끄럽고 비용 없이 흡수하여 자신에게 유리하게 활용할 수 있기 때문이다.
— Attens, DODO DEV 리더
유니스왑 V4가 출시된 후 개발자들 사이에서 금세 폭풍을 일으켰다. 기존의 정형화되고 정적인 풀 계약에 풍부한 확장성을 제공하는 훅(Hook) 구조 덕분이었다. 유니스왑 V4의 훅 인터페이스는 풀과 자금 상호작용의 전 생애주기를 거의 모두 포함하고 있어, 개발자들의 '두뇌 회전'에 풍요로운 땅을 제공한다. 시간가중 선물 시장(TWAMM) 훅처럼 비교적 단순한 예부터 시작해, 시간 가중 오라클을 도입하여 보호 기능을 제공하거나, LimitOrder 훅이 훅의 강력한 잠재력을 보여주는 경우, 해커톤에서 탄생한 크로스체인 거래 훅에 이르기까지 다양한 훅들이 우후죽순처럼 솟아나고 있다.
Defi 업계 종사자로서 우리는 당연히 구경만 하고 있을 수 없었다. 11월에 열린 ETH Global 행사에서 우리는 새로운 훅 설계를 제안했고, 운 좋게도 "Best of Use Hook"상을 수상했다. 그 높은 범용성으로 인해 다양한 유동성 풀 계약에 코드 구성 요소로 장착될 수 있어, 우리는 이를 Aggregator Hook이라 명명했다.
Aggregator Hook은 '다리' 역할을 목표로 한다. 시장의 유동성이 유니스왑 V4 풀에 직접 연결되도록 하며, 동시에 '즉시 원칙(JIT)'을 활용해 유동성 자금을 동적으로 관리한다. 거래 전에 유동성을 주입하고, 거래 후에는 유동성을 철수함으로써 원본 풀에 미치는 영향을 최소화한다. 이러한 통합을 통해 LP들은 전에 없던 편리함으로 자금을 관리할 수 있으며, 유니스왑의 견고한 아키텍처를 활용하면서도 보다 광범위한 DEX 생태계 내 유동성을 이용할 수 있게 된다.
본문에서는 Aggregator Hook의 기원, 작동 메커니즘, 그리고 유니스왑과 더 넓은 DEX 생태계에 미치는 깊은 영향을 다룰 것이다.
I. Aggregator Hook의 탄생
처음 이 아이디어는 우리의 소박한 요구에서 비롯되었다. 효율적인 가격 산정을 제공하는 DODO V3 풀이 있는데, 이것을 훅을 통해 Uni V4로 옮길 수 없을까?
LimitOrderHook을 조사하던 중 우리는 영감을 얻었다. 훅은 일부 자금을 위탁 관리할 수 있다는 점이었다. 사소해 보이는 발견이었지만, 이는 마치 토끼굴 문을 여는 열쇠였다. '훅은 독립된 계약이다'라는 사실과 '훅은 자금을 위탁 관리할 수 있다'는 사실을 합쳐 보면, 훅 자체가 곧 하나의 풀이 될 수 있지 않을까? 지금 우리에게 어떤 계약이 있다고 하자. 이 계약은 다른 DEX의 풀일 수도 있고, 혹은 다른 로직(예: 크라우드펀딩 풀이나 거래 에스크로 시스템 등)을 가질 수도 있다. 그러나 이 계약이 Uni V4의 훅 계약 요구사항을 충족하며, 해당하는 훅 함수를 가지고 있다면, 동시에 훅이 될 수도 있는 것이다.
그렇게 초기 구상이 나왔다. Aggregator Hook은 풀 중심의 훅으로, DODO V3 풀이면서도 동시에 Uni V4의 훅이 되는 것이다. 우리는 DODO V3의 가격 산정 시스템을 훅 함수 안으로 옮겨, JIT를 활용해 유저가 Uni V4에서 거래할 때 DODO V3와 동일한 가격을 받을 수 있도록 하려 했다. 초기 설계는 매우 자연스러웠다. 각 코인의 가격 상한선, 하한선, 판매 대기 수량을 갖고 있다면, 가격 상하한선을 tick으로 변환하고, 판매 대기 수량에 따라 유동성을 채우면 된다. 사용자가 거래하고, 성공! 완벽하다 — 하지만 기다려보라! 우리는 거대한 스프레드(spread) 문제에 부딪혔다.
우리는 다양한 tickSpacing과 Fee 조합을 시도했고, 여러 가격 상하한선에 대응하는 tick 조합도 적용해봤다. 그러나 가장 좋은 결과조차도 원래 DODO V3의 가격 산정과 0.2%의 스프레드 차이를 보였으며, 이는 사용자의 거래 경험에 충분히 영향을 미칠 수 있었다. 분명히 이것은 DODO V3와 Uni V4의 알고리즘이 다르기 때문에 발생한 문제였다. 이 스프레드를 해결하기 위해 두 가지 선택지가 있었다:
-
정문: Uni V4와 DODO V3의 알고리즘 차이를 충분히 고려하여 유동성 재매핑 공식을 찾는다.
-
후문: fromAmount를 이용해 DODO V3에서 가격을 얻고, 이 가격을 기반으로 유동성 채우기 파라미터를 직접 결정하여 스프레드를 제거한다.
짧은 시도 후 우리는 망설임 없이 후문을 선택했다! 물론 첫 번째 방안이 해석 가능한 해를 낼 수 있다면 우아한 소논문 하나를 낼 수 있을 것이라 믿지만, 아쉽게도 우리는 수학자가 아니다. 두 번째 방안의 단점은 분명하다. 반드시 더 많은 가스 소비를 초래할 것이며, 왜냐하면 가격 조회를 한 번 더 추가해야 하기 때문이다. 하지만 장점 역시 매우 분명하다. fromAmount를 이용해 가격을 얻는 함수는 DODO V3만의 고유한 것이 아니다. 모든 DEX 풀에는 유사한 가격 조회 함수가 존재한다. getAmountsOut, get_dy, querySellTokens 등 함수 이름이 무엇이든 간에, 계약에서 가격 정보를 얻는 방법은 언제나 존재한다. 따라서 이 훅 구성 요소는 DODO V3 풀뿐만 아니라 임의의 DEX 풀에도 통합 적용할 수 있으며, 심지어 풀이 아니더라도 가격 산정 기능을 갖춘 '솔버(solver)' 구성 요소로도 사용 가능하다. 유동성을 갖고 있으며, 그 유동성에 대한 가격 산정이 가능한 모든 계약은 이 훅 코드를 추가하기만 하면 원래의 독립된 로직에 영향을 주지 않으면서 동시에 훅이 될 수 있으며, 훅-풀(Hook-Pool)을 통해 합법적인 Uni V4 풀을 구성할 수도 있다. 이상적인 상황에서, Uni V4의 라우팅은 전통적인 Uni V4 풀로만 라우팅되는 것이 아니라, 우리가 만든 이 트릭 풀(trick pool)로도 라우팅될 수 있게 된다.
우리는 흥분했다 — 이것은 다른 Dex들에게도 좋은 일이다. 코드 한 줄만 추가하면 '슈퍼 집계기'에 하나 더 연결할 수 있으니, 왜 하지 않을까? 유니스왑에게도 당연히 좋은 일이다. 타 Dex의 유동성을 매끄럽고 비용 없이 흡수하여 자신에게 유리하게 활용할 수 있기 때문이다.
물론 이런 웅대한 비전은 결국 Uni V4가 어떤 라우팅 알고리즘을 채택하고, 이러한 풀들의 우선순위를 어떻게 처리할지에 달려 있다. 이는 또 다른 주제이며, 아마도 우리는 유니스왑 V4의 라우팅 알고리즘에 대해 별도의 글을 쓸 수 있을 것이다. 그러나 훅의 특수성으로 인해 확실한 것은, 어떤 라우팅 알고리즘이든 간에 훅의 영향을 반드시 고려해야 한다는 점이다. 왜냐하면 훅 내부의 동작은 사용자가 최종적으로 받는 가격과 직접적으로 연결되어 있기 때문이다.
독자들이 이 웅대한 가정의 가장 기본적인 전제를 기억해주길 바란다. 우리는 가격을 얻고, 그 가격을 Uni V4에서 투영할 수 있는 유동성 채우기 방식을 찾아내어, 사용자가 Uni V4를 통해 거래할 때와 훅-풀을 직접 통해 거래할 때 같은 가격을 받을 수 있도록 해야 한다.
결국 우리는 그것을 실현했다. 그래서 우리는 이 기술을 공식적으로 'Aggregator Hook'이라 부르게 된 것이다.
II. Aggregator Hook의 작동 절차
하지만 실제로 Aggregator Hook의 구현 세부사항을 이야기할 때, 당신은 마술이란 결국 연기자의 소매 속에 숨겨진 눈에 잘 띄지 않는 동전에 불과하다는 것을 알게 될 것이다:
-
사용자가 swap 호출을 시작한다.
-
beforeSwapHook이 발동되며, 훅 내에서 세 가지 작업을 수행한다.
-
풀 내에 남아있는 모든 유동성을 제거한다. (쉿! 이것이 핵심 포인트다.)
-
사용자의 fromAmount를 이용해 훅-풀(Hook-Pool) 고유의 거래 가격을 얻는다.
-
거래 가격을 기반으로 modifyPosition 파라미터를 계산하고, 유동성을 채운다.
-
swap 호출이 종료된다.
-
외부 라우터(Router)가 사용자의 입금 및 출금을 처리한다.
아래 그림과 같다:

사용자의 거래 주문이 가까워지면 BeforeSwap 훅이 활성화되고, 유동성 제공자(LPs)는 유니스왑 V4 풀에 유동성을 주입한다. 증가된 유동성은 다가오는 주문에 적응하여 거래 성사에 기여한다. 그러나 거래 종료 시점에 유동성을 철수시키는 대신, 그대로 풀에 남겨둔다. 이렇게 되면 첫 번째 거래의 완전한 생명주기의 일부가 된다.
혁신적인 부분은 후속 거래 처리에 있다. 다음 거래가 시작되기 직전, BeforeSwap 훅이 다시 한 번 트리거된다. 그 첫 번째 임무는 바로 이전 거래에서 남긴 불필요한 유동성을 제거하는 것이다.
잔여 유동성이 철수되면, LP들은 처음의 단계를 반복한다. 새 거래의 수요를 분석하고, 두 개의 가격 구간에 정확한 양의 유동성을 추가한다. 이러한 세밀한 유동성 추가는 유동성이 최적화되어 사용될 뿐 아니라, 가장 높은 효율로 위치 지정되어 슬리피지를 줄이고, 유니스왑 생태계 내 전체적인 거래 실행을 개선한다.
III. 메커니즘과 코드 분석
가장 멋진 부분은 마지막에 압축해서 보여야 한다.
언제 유동성을 제거할 것인가
먼저 왜 유동성을 제거해야 하는지 설명해야 한다. 이는 다음 두 가지 이유 때문이다:
-
이 훅 내에서 훅-풀(Hook-Pool)이 주체이므로, 유동성은 가능한 한 훅-풀에 집중되어야 한다.
-
풀에 여분의 유동성이 없을 때, 가격 재매핑(price remapping) 계산이 비교적 간단하다.
JIT에서는 일반적으로 사용자 스왑 전에 유동성을 충전하고, 스왑 후에 유동성을 인출하는 방식을 취한다. 우리는 처음에는 당연히 그렇게 생각했다. beforeSwapHook에서 유동성을 추가하고 afterSwapHook에서 유동성을 제거하는 것이 논리적으로 매끄럽다. 그러나 훅은 항상 놀라움을 준다. 현재 유니스왑 V4의 최신 testRouter 구조를 살펴보자:



보라, 우리의 위대한 계획을 방해하는 핵심은 바로 사용자가 manager.swap을 완료한 후에야 비로소 poolManager와 실제 토큰 교환이 이루어진다는 점이다. 즉, afterSwapHook은 진짜 '스왑 이후'에 발생하지 못한다는 의미다. 따라서 afterSwapHook 안에서 유동성 제거 작업을 처리할 수 없다. removeLiquidity 함수는 최적의 위치를 잃어버렸고, 우리는 당황하여 버려진 배우처럼 테스트에서 swap 함수 호출 뒤에 홀로 서서 어색하게 튀어나와야 했다. 궁지에 몰린 우리는 swap 함수 내 hookData를 활용해 testRouter를 개조하는 가능성을 고민하고 있었다. 그때 유니스왑의 @ken과 개발자 @saucepoint가 중요한 조언을 해주었다. 바로 다음 거래 시작 시점에 유동성을 제거하라는 것이었다.
이 방안은 우리에게 분명 최선이었다. 첫째, 코드 변경이 극히 적었고, 당시 우리는 해커톤 프레젠테이션까지 단 7시간밖에 남지 않았다. 둘째, 이제 더 이상 불안정한 testRouter에 의존하지 않고 자동으로 유동성을 제거할 필요가 없다. 다음 사용자가 들어올 때까지 잠시 참기만 하면, 이전 거래의 미수금은 그대로 반환된다.
물론 훅-풀의 소유자는 언제든지 유니스왑 V4 내 유동성을 인출할 수 있으며, 반드시 다음 거래 사용자를 기다릴 필요는 없다. 아래 remove 함수는 public이다:


또한 자금 보호를 위해 beforeModifyPosition 훅을 사용해 유동성 수정 요청의 msg.sender를 검증한다.

어떻게 유동성을 채울 것인가
유니스왑 V4와 V3의 계산 공식은 동일하다. 비록 유니스왑 V4의 풀이 라이브러리(lib) 형태로 바뀌었지만 말이다. 우리는 매우 작은 tick 간격 내에서 단방향 유동성만 채우는 것을 고려한다. 예를 들어 tick -46874와 tick -46873 사이에 유동성을 채워 cross-tick 소비를 최소화한다. 코드와 관련 분석 문서를 읽으면 유니스왑의 알고리즘을 통해 다음 공식을 얻을 수 있다.

해당 코드:

하지만 여기서 끝이 아니다. 유니스왑 V3/V4의 유동성 분포를 관찰하면 다음과 같은 사실을 알 수 있다:

유동성을 채운 후, 해당 가격 구간 중 실제 우리 단방향 거래 요구를 충족하는 것은 50%에 불과하다. 만약 token0을 token1로 판매(sell)한다면, 선택한 유동성 채우기 tick 구간은 [tickLower, tickUpper]이며, 이에 대응하는 가격 구간은 [priceLower, priceUpper]이다. 거래 진행 중 예상되는 가격 움직임은 현재 가격에서 목표 가격까지 하락하는 것이다. 그러나 이 목표 가격은 (priceLower + priceUpper)/2보다 낮아서는 안 된다. 만약 (priceLower + priceUpper)/2보다 낮아지면, price_next가 가격 하한선을 초과하게 되며, tick 하한선에 보정(correction)이 필요하게 된다. 반대쪽 거래의 경우에도 보정이 필요하다. 예를 들어:

해당 보정 코드:

가격 균형 (Price Balance)
지금까지 우리가 한 주장은 대부분 실현됐다. Aggregator Hook은 좋지만 완벽하지는 않다. 가장 핵심적인 제한은 거래 방향이다. 이 제한은 다음과 같은 상황으로 설명할 수 있다:
-
현재 V4 풀의 가격이 tick 0에 대응하는 가격에 있다. 이를 currentPrice라 하자.
-
targetPrice > currentPrice일 때, V4 풀은 오직 swap 1 to 0만 수행할 수 있다.
-
targetPrice < currentPrice일 때, V4 풀은 오직 swap 0 to 1만 수행할 수 있다.
이 제한은 직관적으로 이해하기도 쉽다. targetPrice를 시장 가격, currentPrice를 기존 DEX 풀이 거래를 통해 형성한 가격이라고 생각할 수 있다. DEX 풀의 가격이 시장 가격보다 낮을 때, 풀은 token0를 구매하도록 사용자를 요구하여 가격을 높이고 균형을 맞추려 한다. 반대로 DEX 풀의 가격이 시장 가격보다 높을 때, 풀은 token1를 구매하도록 사용자를 요구하여 가격을 낮추고 균형을 맞추려 한다.
이 제한 조건의 코드 표현은 다음과 같다:

IV. 심층 토론 및 전망
우리는 Aggregator Hook의 가장 큰 의미는 훅 설계 관점을 갱신했다는 데 있다고 생각한다.
기존에 훅 설계를 고려할 때 우리는 늘 유니스왑 V4 풀 중심의 관점을 따랐다. 즉, 훅을 사용해 '거래'에 어떤 새로운 기능을 줄 수 있을지 고민했다. Aggregator Hook은 완전히 새로운 관점을 제시한다. 훅의 독립성 덕분에, 이 계약이 반드시 '훅에만 전적으로 의존할 필요는 없다. 훅은 그 계약의 일부 기능일 뿐이며, 유니스왑 V4 풀과의 '다리' 역할을 한다. 그러나 그 자체는 임의의 동작을 수행할 수 있는 계약이며, '거래'와 유니스왑은 그 계약 기능의 일부일 뿐이다.
간단한 예를 들어보자. 일부 소규모 프로젝트팀은 토큰을 크라우드펀딩한 후 일정 비율의 토큰과 자금을 유니스왑에 유동성으로 추가하겠다고 주장한다. 유니스왑 V3 시대에는 크라우드펀딩 계약과 유니스왑 V3 계약의 동작이 비동기적이었고, 사용자들은 프로젝트팀의 자금 이체에 의존할 수밖에 없었다. 그러나 유니스왑 V4 훅이 있으면 어떨까? 우리는 훅의 독립성을 활용해 유동성 추가 동작을 동일한 계약 내에 직접 통합할 수 있지 않을까? 심지어 사용자가 입금할 때 바로 유동성 추가를 실행할 수도 있다(물론 이는 가스 소비가 매우 커지겠지만, 누군가 이런 투명성을 원한다면 말이다). 이건 정말 미친 발상이지만, 훅의 무한한 가능성을 예고한다.
Aggregator Hook의 가장 큰 제한은 바로 거래 시의 가스 소비다. 일반적인 유니스왑 V3의 가스 소비는 약 12만 정도인데, 유동성 증감 작업을 여러 번 수행하기 때문에 Aggregator Hook의 단일 거래 가스는 forge snapshot으로 계산했을 때 약 42~55만 정도로, 일반적인 스왑보다 훨씬 높다. 이는 이런 통합 풀 방식이 저장소 가스에 민감하지 않은 L2에서나, 가스 가격이 매우 낮은 체인에서만 사용될 수밖에 없음을 의미한다. 트릭은 트릭의 대가를 치르는 것이 당연하다.
물론 개선할 여지도 있다. 주로 유동성 충전 및 인출 부분에 있다. 두 가지 가능성이 있으며, 하나는 간단하고 하나는 복잡하다. 먼저 간단한 것부터:
-
가격 균형(Price Balance) 섹션에서 거래 방향 문제를 논의했다. 유동성 제거 방안을 유지한다면 targetPrice와 currentPrice의 규칙은 바꿀 수 없지만, tick0과 limitTick이 같지 않아야 할 이유는 없다. 다만 limitTick = tick0일 경우 유동성 충전 시 양방향 충전이 되므로, 유동성 충전량 계산 공식을 변경해야 한다. 이 변경을 통해 거래 방향 제한을 약간 완화할 수 있다. 한 틱만큼.
더 복잡한 것은:
-
이미 양방향 유동성 충전을 고려했으니, 왜 끝까지 밀고 가지 않을까? 더 이상 유동성 제거를 강제하지 말고, 훅-풀 관리자가 필요할 때만 removeLiquidity를 실행하도록 하자. 이를 통해 사용자 스왑 시 가스 소비를 더욱 줄이고, Aggregator Hook을 더 '사용 가능하게' 만들 수 있다. 물론 이는 앞선 간단한 방안보다 훨씬 복잡한 유동성 충전 알고리즘을 유발할 가능성이 크다.
현재 Aggregator Hook 풀의 소스 코드는 다음과 같습니다:
https://github.com/Attens1423/Aggregator-Hook
거래 결과를 직관적으로 보기 위해 afterSwap 훅 호출 시 출력 로그를 추가했지만, 이는 필수 사항이 아니다. 내부에는 많은 중간값 출력이 포함되어 있으며, forge test를 사용해 Aggregator Hook의 동작을 확인할 수 있다.
DODO V3가 아마도 이 '게걸스러운 게'를 먹는 첫 번째 풀이 될 것이다. 우리의 다음 계획은 Aggregator Hook을 DODO V3 풀에 통합하는 것이다. 여기에는 일부 필수적인 풀 리저브(pool reserve) 및 풀 상태(pool state) 업데이트 작업이 필요하며, 모든 테스트를 완료한 후 오픈소스로 공개하여 초기의 훅-풀(Hook-Pool) 구상을 실현할 예정이다.
TechFlow 공식 커뮤니티에 오신 것을 환영합니다
Telegram 구독 그룹:https://t.me/TechFlowDaily
트위터 공식 계정:https://x.com/TechFlowPost
트위터 영어 계정:https://x.com/BlockFlow_News













