스왑이 남긴 MEV를 사용자에게 돌려줄 수 있을까
2022년, DeFi에서 스왑은 몇 초 안에 끝났지만 그 거래의 경제적 여파는 거기서 끝나지 않았다.
한 사용자가 AMM에서 큰 수량을 스왑한다. 풀의 준비금 비율이 움직이고, 다른 DEX와의 가격 차이가 벌어진다. 그 차이를 발견한 searcher는 곧바로 반대 거래를 넣는다. 조금 전까지 어긋나 있던 두 가격은 다시 가까워지고, searcher는 그 과정에서 차익을 얻는다.
시장은 의도한 대로 작동한 것처럼 보인다. 차익거래가 가격을 다시 맞췄기 때문이다. 하지만 거래의 당사자에게는 묘한 결말이 남는다. 가격 차이를 만든 사람은 사용자였고, 사용자는 슬리피지와 가스비를 부담했다. 정작 그 거래가 만든 차익은 가장 빨리 뒤따라온 다른 누군가에게 돌아갔다.
2022년 말, 우리가 던졌던 질문은 여기서 시작했다.
그 차익거래가 사용자의 트랜잭션 밖에서 기다리지 않게 만들 수는 없을까? 프로토콜이 먼저 차익을 회수하고, 남은 가치를 사용자에게 돌려줄 수는 없을까?
이 글은 그때의 질문과 설계를 다시 꺼내 보는 기록이다. 당시에는 이를 “MEV로 인한 거래 왜곡 방지”라고 불렀다. 몇 해가 지나 관련 연구와 여러 프로토콜을 함께 놓고 보면, 그 표현은 너무 컸다. 더 정확히 말하면 이 설계는 모든 MEV를 없애려는 해법이 아니라, 사용자 스왑이 만든 일부 backrun 가치를 트랜잭션 안으로 가져오려는 시도였다.
네 개의 풀에서 시작한 질문
당시 내부 시뮬레이션에는 같은 토큰 쌍을 가진 네 개의 constant-product 풀이 있었다. 각 풀의 준비금을 간단한 숫자로 놓고, 한쪽 풀에서 스왑이 일어난 뒤 서로 달라진 비율을 어떻게 다시 맞출지 계산했다.
스프레드시트에는 복잡한 시장도, 정교한 searcher 경쟁도 없었다. 풀마다 a * b = k를 유지한다는
가장 단순한 AMM만 있었다. 그러나 그 작은 모델은 중요한 질문 하나를 선명하게 보여줬다.
사용자의 스왑 뒤에 가격 차이가 남는다면, 외부 차익거래자가 그 차이를 닫기 전에 스왑을 실행한 프로토콜이 직접 반대 거래를 수행할 수 있다. 그리고 그 실행에서 얻은 순잉여를 사용자에게 돌려주면, 같은 가격 회복 과정이라도 가치의 최종 귀속은 달라진다.
차익거래를 제거하는 것이 아니다. 차익거래를 누가 수행하고, 그 결과를 누가 가져가는지 바꾸는 것이다.
관찰에서 환원까지
설계의 중심에는 둘 이상의 DEX를 바라보는 aggregator가 있다. 실행 흐름을 특허 명세서의 구성요소가 아니라 실제 동작 순서로 옮기면 다음과 같다.
- 동일한 토큰 쌍을 가진 여러 DEX의 준비금과 스왑 가격을 확인한다.
- 사용자 스왑 전후의 비율 변화와 DEX 사이의 가격 차이를 감지한다.
- 교차 스왑으로 얻을 수 있는 총차익과 모든 실행 비용을 계산한다.
- 순이익이 양수일 때만 상대적으로 고평가된 토큰을 플래시론으로 빌린다.
- 고평가된 DEX에서 팔고 저평가된 DEX에서 되사는 교차 스왑을 실행한다.
- 플래시론을 상환하고 남은 순잉여를 사용자의 가격 개선 또는 환급으로 처리한다.
플래시론은 없는 수익을 만들어내는 장치가 아니다. 이미 존재하는 가격 차이를 자기자본 없이 한 번의 실행 흐름에서 닫을 수 있게 해주는 유동성 수단이다. 대출금과 프리미엄을 같은 트랜잭션 안에서 갚지 못하면 전체 실행이 되돌아간다. Uniswap v2의 flash swap 문서가 차익거래를 대표적인 사용 사례로 드는 이유도 여기에 있다.
따라서 “차익이 있다”는 것만으로는 실행 조건이 되지 않는다. 다음 값이 양수여야 한다.
사용자 가격 개선분 = 교차 DEX 총차익
- DEX 수수료
- 플래시론 프리미엄
- 가스비와 실행 오버헤드
- 프로토콜 수수료(있는 경우)
실제 구현이라면 이 값이 최소 이익 기준보다 작거나 사용자의 minOut을 만족하지 못할 때 실행 전체를
중단해야 한다.
숫자로 끝까지 계산해 보기
사용자 스왑이 끝난 직후, 같은 A/B 토큰 쌍을 가진 두 풀이 다음 상태라고 가정해 보자.
| 풀 | A 준비금 | B 준비금 | A의 한계가격 (B/A) |
|---|---|---|---|
| DEX A | 100 | 100 | 1.0 B |
| DEX B | 100 | 120 | 1.2 B |
DEX B에서 A가 상대적으로 비싸므로 A를 빌려 DEX B에 팔고, 받은 B로 DEX A에서 A를 되사는 방향이
후보다. 두 풀의 스왑 수수료를 각각 0.3%라고 하면 입력량 q에 대한 출력량은 다음과 같다.
gamma = 1 - 0.003 = 0.997
amountOut = reserveOut * gamma * q
/ (reserveIn + gamma * q)
플래시론 프리미엄을 0.05%, 가스비를 A로 환산해 0.01 A라고 가정하자. 이 조건에서 순이익을 가장 크게 만드는 대출량은 약 4.196 A이고, 설명을 위해 4.2 A로 반올림하면 계산은 다음과 같다.
1. 4.2 A를 플래시론으로 대출
2. DEX B에서 4.2 A -> 4.822925 B
3. DEX A에서 4.822925 B -> 4.587851 A
교차 스왑 총차익 4.587851 - 4.2 = 0.387851 A
플래시론 프리미엄 4.2 * 0.0005 = 0.002100 A
가스비 환산 0.010000 A
사용자에게 돌릴 순잉여 0.375751 A
두 번의 0.3% DEX 수수료는 각 amountOut 계산에 이미 들어 있다. 프로토콜 수수료가 없고 잉여를 모두
A로 지급한다면 사용자가 받는 개선분은 약 0.375751 A다. 잉여를 사용자의 출력 토큰인 B로 바꾸어
지급한다면 그 변환의 수수료와 가격 영향까지 다시 계산해야 한다.
거래 뒤 A의 한계가격은 DEX A에서 약 1.0986 B, DEX B에서 약 1.1053 B가 된다. 가격이 완전히 같아지지 않는 것은 오류가 아니다. 수수료가 있는 시장에서는 두 번의 스왑 비용보다 작은 가격 차이는 더 이상 수익성 있는 차익거래가 아니므로 no-arbitrage band가 남는다. 이 예제는 “모든 풀을 같은 비율로 맞춘다”보다 “비용을 포함한 순이익을 최대화한다”가 실제 실행 조건에 더 가깝다는 점을 보여준다.
실행 경로를 코드로 옮기면
실행 전 시뮬레이션과 실제 onchain 실행의 경계를 포함한 의사코드는 다음과 같이 잡을 수 있다.
function swapWithInternalizedArbitrage(order):
snapshot = readEligiblePools(order.pair)
baseline = simulateBestUserRoute(order, snapshot)
candidate = optimizeCrossDexTrade(baseline.postSwapState)
if candidate.netSurplus <= MIN_PROFIT:
return executeBaselineSwap(order, baseline)
result = atomicExecute([
validateQuotes(snapshot, order.deadline),
executeUserSwap(order, baseline.route),
flashBorrow(candidate.borrowToken, candidate.amount),
executeCrossDexTrade(candidate.route),
repayFlashLoan(),
convertSurplusTo(order.tokenOut),
refundSurplus(order.owner),
])
require(result.totalUserOut >= order.minOut)
require(result.totalUserOut >= baseline.userOut)
require(result.flashLoanRepaid)
require(result.netSurplus > MIN_PROFIT)
require(result.unaccountedBalance <= DUST_LIMIT)
baseline.userOut은 가능하면 실행 시점의 상태 또는 유효기간이 있는 서명된 quote에서 계산해야 한다.
그렇지 않으면 시뮬레이션과 블록 포함 사이에 가격이 바뀌었을 때 지킬 수 없는 기준이 된다. 또한 환급
토큰을 사용자의 출력 토큰으로 통일하지 않는다면 totalUserOut을 비교할 신뢰할 수 있는 가격 기준이
필요하다.
이 의사코드에서 핵심은 차익거래 성공 자체가 아니라 다음 불변식을 함께 지키는 것이다.
- 내부화 경로가 없을 때보다 사용자의 결과가 나빠지지 않는다.
- 플래시론 원금과 프리미엄은 같은 실행에서 전액 상환된다.
- 모든 비용을 제한 뒤에도 최소 순잉여가 남는다.
- 잔여 토큰은 사용자 환급, 명시된 수수료 또는 허용된 dust 중 하나로 전부 회계 처리된다.
- 어느 조건이든 깨지면 사용자 스왑을 포함한 전체 경로를 되돌리거나, 안전하게 baseline 경로만 실행한다.
수식은 정답이 아니라 출발점이었다
2022년 모델은 각 풀의 두 토큰 준비금을 a_i, b_i라고 두고 다음의 관계에서 시작했다.
k_i = a_i * b_i
A = sum(a_i)
B = sum(b_i)
S = A / B
여기서 S는 참조하는 모든 풀의 준비금을 합산해 만든 목표 비율이다. 각 풀의 invariant를 유지하면서
준비금 비율을 S에 맞추기 위한 목표값은 다음과 같이 계산했다.
a_i' = sqrt(S * a_i * b_i)
b_i' = (a_i * b_i) / a_i'
필요한 거래 방향과 수량 = a_i' - a_i
a_i' - a_i가 양수라면 해당 풀에는 토큰 A를 넣는 방향의 거래가 필요하고, 음수라면 반대 방향의
거래가 필요하다. 여러 풀에서 필요한 양을 합산해 어느 토큰을 얼마나 빌릴지 결정하고, 교차 스왑 뒤
대출을 갚는다는 구상이었다.
수식은 간결하다. 바로 그 때문에 가정도 많다.
우선 이 모델은 수수료가 없는 constant-product AMM을 가정한다. 오늘날 흔한 concentrated liquidity,
서로 다른 fee tier, stable-swap curve는 반영하지 않는다. S=A/B 또한 외부 시장의 공정가격이 아니다.
참조 풀의 준비금으로 만든 내부 기준일 뿐이며, 어떤 풀을 선택하느냐와 준비금 조작에 민감하다. 토큰 A의
가격을 토큰 B로 표시할 때는 준비금 비율과 가격의 방향이 역수가 될 수 있으므로 표기 규칙도 명확히 해야
한다.
각 풀의 k가 보존된다는 사실만 확인해서도 부족하다. 전체 경로에서 토큰이 보존되는지, 대출 원금과
프리미엄을 갚은 뒤 무엇이 남는지, 그 잔여분을 어느 토큰으로 사용자에게 지급할지까지 하나의 원장처럼
맞춰봐야 한다.
그래서 이 수식은 완성된 최적화 알고리즘이라기보다 어디서 차익이 생기고 어느 방향으로 거래해야 하는지를 드러내는 첫 모델로 보는 편이 정확하다.
트랜잭션 안으로 가져온다는 것
이 접근의 가장 중요한 경계는 원자성이다.
사용자 스왑과 뒤따르는 대출·교차 스왑·상환을 하나의 트랜잭션으로 묶을 수 있다면, 외부 참여자가 그 내부 호출 사이에 자기 거래를 끼워 넣을 수는 없다. 어느 단계에서든 수익 조건이나 상환 조건을 만족하지 못하면 전체 트랜잭션을 되돌릴 수도 있다.
builder가 결정하는 블록 내 순서
┌──────────────────────────────────────────────────────────┐
│ 공격자 트랜잭션: 전체 경로 앞에 배치 가능 │
├──────────────────────────────────────────────────────────┤
│ 우리의 단일 트랜잭션 │
│ quote 검증 -> 사용자 스왑 -> flash borrow -> 교차 스왑 │
│ -> 상환 -> 사용자 환급 │
│ 위 호출 사이에는 외부 거래가 들어올 수 없음 │
├──────────────────────────────────────────────────────────┤
│ 공격자 트랜잭션: 전체 경로 뒤에 배치 가능 │
└──────────────────────────────────────────────────────────┘
하지만 이것이 거래 순서 문제 전체를 해결한다는 뜻은 아니다. builder나 proposer는 여전히 이 트랜잭션의 앞이나 뒤에 다른 트랜잭션을 놓을 수 있다. 사용자의 주문이 공개 멤풀에 먼저 노출된다면 트랜잭션 전체를 대상으로 한 frontrun이나 sandwich도 남는다. 내부 단계의 원자성과 블록 안에서의 transaction-level ordering 보호는 서로 다른 문제다.
private order flow, batch auction, intent와 builder 수준의 보호가 필요한 이유가 여기에 있다. CoW Protocol은 batch auction과 Coincidence of Wants를 사용하고, Flashbots는 private transaction과 MEV 수익 공유를 별도의 축에서 다룬다. 이들은 이 글의 리밸런싱 설계와 경쟁하는 하나의 정답이라기보다, 서로 다른 공격면을 다루는 설계들이다.
이 설계가 최소한 가정해야 할 threat model을 실행 경계에 맞춰 정리하면 다음과 같다.
| 공격면 | 가능한 실패 | 필요한 방어 |
|---|---|---|
| Builder / proposer | 전체 트랜잭션 앞뒤에 거래를 배치하거나 포함을 지연 | private submission, deadline, minOut, quote 유효성 검사 |
| 얕거나 조작된 참조 풀 | 준비금을 순간적으로 바꿔 잘못된 목표 가격과 거래량 유도 | 최소 유동성, 가격 편차 제한, 외부 기준가격, 풀별 가중치 |
| 악성 DEX adapter | 거짓 반환값, 임의 callback, 재진입으로 잔액 탈취 | adapter allowlist, balance-delta 회계, reentrancy guard |
| Stale state | 시뮬레이션 이후 reserve가 변해 수익 또는 상환 조건 붕괴 | 실행 시 재검증, 최소 순이익 조건, 전체 revert |
| 비표준 토큰 | fee-on-transfer, rebasing, callback으로 예상 수량 불일치 | 지원 토큰 제한, 실제 잔액 변화 기준 정산, dust 한도 |
여기서 contract가 직접 보장할 수 있는 것은 가운데 단일 트랜잭션의 상태 전이와 잔액 불변식이다. 그 밖의 주문 노출과 블록 순서는 별도의 전달 경로와 시장 구조가 담당해야 한다.
그래서 어떤 MEV가 줄어드는가
이 설계가 직접 겨냥하는 것은 좁다.
- 사용자 스왑 직후 여러 DEX 사이에 남는 residual arbitrage
- 그 차익을 가져가는 외부 backrun 경쟁
- 비용을 제외한 잉여가 사용자에게 돌아가지 않는 가치 배분
반대로 다음 문제는 그대로 남는다.
- 전체 트랜잭션을 감싸는 frontrun과 sandwich
- builder의 거래 포함·제외·재배치와 censorship
- liquidation, oracle manipulation 등 다른 종류의 MEV
- 조작된 참조 풀이나 잘못된 목표 비율
- stale quote, 악성 adapter, callback과 reentrancy 위험
- 수수료와 가스비가 차익보다 커지는 실행 실패
당시 문서에 썼던 “MEV 피해 방지”나 “슬리피지 제거”를 지금 그대로 반복하지 않는 이유다. 더 적절한 표현은 swap-induced arbitrage의 내부화다. 이 접근은 차익거래의 경제적 필요 자체를 없애지 않는다. 다만 그 가치가 외부 searcher에게만 흘러가지 않도록 실행 경로와 귀속 규칙을 바꾼다.
이 차이는 LP 관점에서도 중요하다. Loss-Versus-Rebalancing 연구가 보여주듯, AMM과 외부 시장 사이의 가격 차이에서 발생하는 arbitrage는 LP의 성과와도 연결된다. 차익을 사용자에게 환원한다고 해서 underlying adverse selection이 사라지는 것은 아니다. 누가 이익을 얻는지는 바뀔 수 있지만, 누구의 가치에서 그 이익이 나왔는지는 별도로 측정해야 한다.
2022년의 질문은 우리만의 것이 아니었다
이 아이디어를 “최초”라고 부를 수는 없다. 비슷한 시기 같은 방향의 질문이 이미 여러 곳에서 등장했다.
2022년 8월 공개된 WOWMAX의 설명은 swap과 arbitrage를 하나의 atomic transaction으로 실행하고, DEX 사이의 가격을 리밸런싱한 뒤 이익을 거래자에게 돌려주는 구상을 소개했다. 이후 UniswapX whitepaper도 filler 경쟁과 routing, batching을 통해 MEV를 내부화하고 surplus를 가격 개선으로 돌리는 방향을 제시했다.
구현은 서로 다르지만 공통된 질문이 있다.
사용자 주문이 만든 잉여는 반드시 가장 빠른 searcher의 몫이어야 하는가?
2022년의 작은 스프레드시트가 흥미로운 이유도 독점적인 정답을 갖고 있어서가 아니다. 여러 설계가 같은 시기에 같은 질문으로 수렴했다는 사실, 그리고 프로토콜이 주문의 실행 방식뿐 아니라 실행 후 남는 가치의 귀속 방식까지 설계할 수 있다는 사실을 보여주기 때문이다.
특허, 그리고 공개
이 글의 설계는 뱅크웨어글로벌 주식회사가 보유한 대한민국 등록특허 제10-2564770호 「거래 왜곡을 방지하기 위한 탈중앙화 거래소를 통한 토큰 스왑 시스템 및 방법」에 기술되어 있다. 출원일은 2023년 1월 12일, 등록일은 2023년 8월 3일이다.
여기서 특허 등록 사실은 기술적 완전성의 증명이나 글의 권위를 위한 장식이 아니다. 2022년에 시작한 설계가 어떤 형태로 정리되고 공개되었는지 출처와 이력을 투명하게 밝히기 위한 정보다. 이 글 또한 특허의 실시허락이나 권리 불행사 선언을 대신하지 않는다. 기술을 공공재에 가깝게 공개하려면 오픈소스 코드와 함께 방어적 권리 행사 원칙, patent pledge 또는 명시적인 라이선스 범위를 별도로 정해야 한다.
블록체인 생태계에서 중요한 것은 특허의 존재 자체보다 그 권리를 어떻게 사용할 것인지에 가깝다. 이 기록의 목적은 권리를 앞세우는 것이 아니라, 아이디어의 가정과 한계까지 공개해 다음 사람이 더 나은 모델을 만들 수 있게 하는 데 있다.
다시, 스왑이 끝난 뒤
2022년의 질문으로 돌아가 보자.
사용자의 스왑이 끝난 뒤 가격 차이가 남고, 누군가 그 차이를 닫아야 한다면, 그 과정에서 생기는 가치는 누구에게 돌아가야 할까?
이 설계는 MEV를 없애지 못한다. 외부의 모든 ordering 권한도 통제하지 못한다. 단순한 준비금 비율을 공정가격으로 믿어서도 안 된다. 하지만 한 가지 선택지는 분명하게 제시한다. 사용자 행동이 만든 차익을 외부 경쟁의 전리품으로만 남겨두지 않고, 프로토콜 실행 안으로 가져와 사용자에게 다시 돌려줄 수 있다는 선택지다.
스왑은 몇 초 안에 끝난다. 그러나 그 스왑이 만든 가치가 어디로 가는지는 코드가 정한다.
2022년에 시작한 우리의 질문은 바로 그 코드에 관한 것이었다.