탈중앙화 거래소 사례로 배워보는 Front Running 공격

Synthetix와 aFan 사례를 통해 알아보는 Front Running 공격과 해결방안

Lee Kyutaek
HAECHI AUDIT
17 min readJan 9, 2020

--

2019년 6월, Synthetix에서 Front-Running과 관련된 자금 탈취 사건이 알려졌습니다.

과연 어떠한 방식으로 자금 탈취가 가능했으며, 현재 이 문제를 해결하기 위해 제안된 해결 방안에는 무엇이 있을까요?

이번 글에서는 해당 탈취 사건이 발생된 이유, 피해금액, 해결과정, 현재 적용중인 해결법에 대해 Synthetix와 어팬(aFan) 서비스의 사례를 통해 자세히 알아보도록 하겠습니다.

Synthetix란?

Synthetix는 단순하게 말하면, 탈중앙화된 거래소지만, Synth이라는 독특한 개념을 가지고 있습니다.

Synthetix의 기축통화인 SNX를 가지고 있는 사용자는 SNX를 이용하여 USD, EUR 등의 법정화폐혹은 ETH, BTC등에 대응하는 Synth(sUSD, sEUR, sETH, sBTC)들을 발행 할 수 있습니다. 그리고 Synthetix Exchange를 통해 Oracle 가격을 이용한 Synth 사이의 교환을 하게 됩니다.

하지만 Synth 사이의 교환은 해당 자산을 완전히 나타내는 것은 아닙니다. Synth와 가격만 일치할 뿐이지 실제 자산이 있는 것은 아니기 때문입니다. 다시 말해서 sETH의 가격은 ETH와 가격이 동일하지만, sETH를 보유하고 있는 것이 ETH를 보유하는 것이 아니라는 것입니다. 따라서 Synthetix 커뮤니티는 sETH와 ETH를 Uniswap에서 1:1 교환하게 함으로써 실제 ETH와의 교환성을 확보하게 되었습니다. 또한 Synthetix의 쉬운 Onboarding과 실제 ETH와 쉽게 교환할 수 있도록 지원함으로써 sETH와 ETH의 유동성은 Uniswap 전체 유동성의 약 1/3을 차지하게 되었습니다.

Synthetix에서의 교환 비율

Synthetix는 sUSD, sEUR과 같은 Synth간의 교환을 허용하기 위해 각각의 가격을 ExchangeRates라는 하나의 스마트 컨트랙트에서 관리하도록 했습니다.

그리고 주기적으로 트랜잭션을 발생 시켜 ExchangeRates에 Synth들의 가격을 업데이트 해주는 방식을 택하고 있습니다.

만약 Synthetix를 이용하는 트레이더가 ExchangeRates 비율이 업데이트 되기 전에, 업데이트 될 값을 알게 된다면 어떤 일이 발생할까요?

Synthetix는 기존의 다른 거래소와는 다르게, SNX라는 기축통화를 통해 발행된 Synth들로 거래가 진행됩니다. 따라서 실제 USD, BTC, ETH등의 화폐가 실제 거래소에 존재하지 않더라도 거래가 가능합니다. 대신, 외부에서 해당 암호화폐의 가격정보를 받아오고 Synth들끼리 교환을 허용하도록 하는 구조입니다.

다시 말해서 기존 암호화폐 거래소와는 다르게 오더북 형태로 거래가 진행되는 것이 아니라, Exchange Rates에 의존해서 모든 매수/매도 가격이 결정됩니다.

따라서 만약 트레이더가 Exchange Rates 비율이 업데이트 되기 전에 해당 값을 알게 된다면, “확정적으로” 가격이 오를지, 내려갈지를 알 수 있게됩니다다. 특정 토큰이 가격이 오른다는 것을 미리 알게 된다면, 비율 업데이트 전에 토큰을 매수하고 업데이트 후에 매도하여 차액에 대한 이익을 실현할 수 있고, 보유한 토큰의 가격이 내려간다는 것을 미리 알게 된다면, 비율 업데이트 이전에 매도하여 손실을 감소할 수 있게 될 것입니다.

토큰 가격은 외부에 의해 정해지다보니, 업데이트 마다 가격이 상승하는 토큰이 있을 것이고, 하락하는 토큰이 있을 것 입니다. 이를 이용하여 업데이트 마다 0.1%의 수익을 낸다고 가정하면, 10번의 업데이트 후에는 약 1%의 수익, 100번의 업데이트 후에는 약 10%의 수익, 1000번의 업데이트 후에는 약 170%의 수익이 발생하게 됩니다.

그렇다면 Synthetix 트레이더는 어떻게 비율 업데이트 값을 업데이트 전에 알 수 있는 것일까요?

Exchange Rates 비율을 업데이트 하는 방식에 그 해답이 있었는데요, 비율 업데이트 하는 방식을 간단히 살펴보면 트랜잭션을 발생시켜 해당 비율 값을 변경시키는 방법으로 진행되고 있습니다. 좀 더 자세히 알아보기 위해 트랜잭션을 발생시킬 때 서명을 하고, 해당 트랜잭션이 블록체인에 기록되는 방식을 살펴보도록 하겠습니다.

트랜잭션의 LifeCycle

이더리움 블록체인 상에 트랜잭션이 기록되기 까지 다음과 같은 단계를 거치게됩니다.

  1. 사용자 A가 해당 트랜잭션에 서명합니다
  2. 서명된 트랜잭션은 A가 지정한 이더리움 노드에 전달됩니다
  3. 해당 노드에서 트랜잭션의 유효성을 검증합니다
  4. 유효한 트랜잭션의 경우 연결된 다른 노드들에게 트랜잭션을 전파합니다
  5. 채굴자가 tx pool에 들어온 트랜잭션들을 모아 채굴을 진행합니다
  6. 채굴에 성공하면, 트랜잭션이 블록체인에 기록됩니다

(물론, 채굴에 성공한다고 하더라도, uncle block 이 될 수 있습니다)

트랜잭션은 여러단계를 거쳐 전파되며 전파 받는 모든 이더리움 노드들이 서명을 검증해야 하므로. 누구나 해당 트랜잭션의 내용을 확인 할 수 있습니다.

또한, 이더리움 노드 에서는 JSON-RPC 혹은 console의 형태로 전달받은 트랜잭션중에 아직 처리되지 않은 트랜잭션을 확인 할 수 있는 API가 존재합니다.

그리고 채굴자가 채굴 시도하는 블록에 담겨져 있는 트랜잭션은, 발생한 시간순서 or tx pool로 들어온 시간순서에 따라 담기지 않습니다. 트랜잭션을 서명하는 단계에서, 단위연산에 해당하는 가격을 매겨 트랜잭션을 발생 시킬 수 있는데, 해당 가격이 높을 수록 블록에 먼저 담길 확률이 올라갑니다.

트랜잭션의 라이프 사이클을 고려하면, 다음과 같은 공격이 가능합니다.

  1. 사용자 A 가 해당 트랜잭션을 가스비 3의 가격으로 서명합니다
  2. 서명된 트랜잭션은 A가 지정한 이더리움 노드에 전달됩니다
  3. 해당 노드에서 트랜잭션의 유효성을 검증합니다
  4. 유효한 트랜잭션의 경우 연결된 다른 노드들에게 트랜잭션을 전파합니다
  5. 공격자 B 가 해당 트랜잭션을 발견하고, 트랜잭션 이후 변화될 값을 확인합니다
  6. 변화 이전에 본인에게 이익을 안겨줄 수 있는 트랜잭션을 3보다 높은 가격(ex.5)으로 서명합니다
  7. 서명된 트랜잭션을 B가 지정한 이더리움 노드에 전달합니다
  8. 해당 노드가 유효성을 검증합니다
  9. 유효한 트랜잭션의 경우 연결된 다른 노드들에게 트랜잭션 전파됩니다
  10. 채굴자의 tx pool에 A의 트랜잭션과 B의 트랜잭션이 모두 존재합니다
  11. 채굴자는 가격이 높은 B의 트랜잭션을 먼저 처리합니다
  12. 채굴에 성공하고, 트랜잭션 B -> A순으로 블록체인에 기록됩니다

이런 방식으로 공격자 B가 공격에 성공하여 이득을 얻는 경우에, 이런 공격을 Front-Running공격이라고 합니다. (비유하자면 새치기 같은 공격인거죠)

트레이더의 Front Running 공격

그럼 토큰의 가격이 변화하는 상황에 대해 생각해보겠습니다. 트레이더 입장에서는 가격이 오를 토큰은 구매하고, 내려가는 토큰은 파는 것이 효율적일 것입니다.

최적의 효율을 위해 트레이더는 다음과 같은 행동을 하는 트레이딩 봇을 개발합니다.

  1. 노드가 가지고 있는 tx pool을 관찰한다.
  2. ExcahngeRates에 변화가 생기는 트랜잭션( tx A )이 존재하는지 확인한다.
  3. 존재하는 경우, 보유하고 있는 토큰 중, 가격이 내려가는 토큰을 가격이 올라가는 토큰으로 모두 교환하는 함수를 A보다 높은 가격으로 호출하는 트랜잭션을 생성한다.
  4. 다시 1로 돌아간다.

단순한 Front Running을 반복하는 트레이딩 봇 이지만, 취급하는 토큰이 많다는 점과, 약 20블록 간격으로 ExchangeRate에 변화가 생긴다는 점을 감안하면, 변화가 생길때마다 0.1%의 이익만 얻는다 해도 하루에 약 33%의 이익, 한달에는 5000배 가량의 자금 이익이 생길 수 있습니다.

자금탈취

실제로 위와 같은 트레이딩 봇을 구축하고 있던 사용자가 있었습니다. 확인되는 바로는 약 7일 정도 아무 문제없이 봇을 실행한 것으로 확인됩니다. 하지만, 문제는 ExchangeRates가 오류를 발생하면서 생겼습니다.

당시 ETH의 원화 가격은 30만원 중~후반대를 형성하고 있었습니다. 그런데, 일순간의 업데이트 오류로 약 700원 정도에 거래가 가능해졌습니다.

ExchangeRates는 8021796 블록에서 문제가 생겼습니다. 그 전후로 업데이트 된 시점은, 8021784/8021810 번 블록 입니다.

실제로 거래 비율은, 다음과 같이 업데이트 되었습니다.

(iETH, sKRW, sETH의 가격은 Synthetix에서 정한 단위 가격을 기준으로 각각의 토큰의 가격을 표현한 것입이다)

8021796블록 이전에 iETH를 갖고 있던 트레이딩 봇은, sKRW의 가격이 폭등한다는 것을 알게되어 갖고있던 iETH전량을 sKRW로 교환하는 트랜잭션을 발생 시킵니다. 그리고 ExchangeRates를 변경하는 트랜잭션은 같은 블록에 담기지만, 더 낮은 순위로 들어가게 되어 트레이딩 봇은 가격 폭등 직전에 교환에 성공 하여 약 2574억개의 sKRW로 교환 하였습니다.

그리고 바로 다음 업데이트인 8021810블럭에서 트레이딩 봇은 다시 sKRW 를 sETH로 교환하여 35,759,524개의 이더리움(당시 가격 약 13조원)에 해당하는 sETH를 얻게 됩니다.

문제의 해결

다행스럽게도, 해당 트레이딩 봇을 돌리던 트레이더는 Synthetix의 도움 요청에 응하였고, sETH를 돌려줍니다. Front Running을 하려던 것이지, Oracle이 잘못된 것으로 부당한 이득을 얻으려고 하진 않았다는 이유였습니다.

대신, Synthetix는 Front Running을 진행하는 트레이딩 봇을 계속 돌릴 수 있도록 허용하고, 사고로 얻은 sETH가 아닌 모든 잔고는 그대로 두는 조건으로 진행했다고 합니다. 추가적으로 Bug Bounty의 개념으로 상금도 전달했다고 합니다.

또한, sETH가 결국 SNX를 기반으로 하다보니, SNX의 시가 총액을 넘는 금액을 인출 할 순 없었다고 합니다😅. 물론 Uniswap등 다른 거래소로 Exit 할 수는 있었겠죠?

또 다른 문제

약 3달 후, Reddit의 Ethereum 서브레딧에 위의 트레이더가 을 올립니다. 정리하자면, 자신의 잔고를 Synthetix 측에서 삭제했다는 내용이었습니다.

방법은 다음과 같았다고 합니다.

  1. Oracle이 ExchangeRate을 업데이트 하는 TX를 발생시킵니다.
  2. 트레이더가 Front Running을 실행합니다.
  3. Oracle이 Front Running이 실행된 걸 확인합니다.
  4. Front Running 트랜잭션보다 높은 가격으로, 일시적으로 수수료를 매우 높게(99%) 하는 트랜잭션을 발생시킵니다.
  5. Front Running 트랜잭션보다 먼저 실행되고, 트레이더는 대부분의 금액(99%)을 수수료로 사용하게 됩니다.

결국 Front-Running에 Front-Running으로 대응 한 셈입니다.

해당 글에 대해 Synthetix의 CEO가 서브레딧에 답변 글을 달기도 했습니다. 주요 골자는 “해당 트레이더는 시스템을 공격하려 한 것이므로 Slashing 된 것이고 PoS에서 사용되는 개념과 비슷하다. 또한 이 선택은 우리가 아닌 Oracle이 한 것이다” 라는 내용이었죠.

SIP(Synthetix Improvement Proposal)-6를 보게 되면 해당 Oracle에 관한 논의 내용을 확인 할 수 있습니다. 하지만 실제 코드 및 로직은 Closed Source로 구현 되어있고, 자세한 논의 내용을 Discord링크로만 대체했다 보니, 어떻게 구현되어있을지는 미지수 입니다🤔.

Defi(Decentralized Finance) 서비스를 제공하는 팀에서 중앙화 되어있는 방식으로 대응했다는 점에서 커뮤니티에 큰 반발을 사기도 했습니다.

레딧에 올라온 트레이더의 글과, Synthetix CEO의 글을 봤을 때, Front Running에 대응하는 Oracle 도입 이후에도 Front Running을 완전히 막지 못한 것으로 보입니다. 반면 Oracle이 트레이더에 부과했던 Slashing은 쉽게 막혔다고 합니다.

결국 Synthetix측은, Front Running이 일어날 수 없게, ExchangeRates가 업데이트되는 트랜잭션 발생 전, 거래를 중지하는 트랜잭션을 발생하고 업데이트 시키는 방식으로 변경하게 됩니다(https://sips.synthetix.io/sips/sip-7). 또한, Oracle 문제에 대해 중앙화 되어있어서 발생했던 문제였던 만큼, Chainlink라는 탈중앙화 Oracle네트워크를 이용하여 해결하고 있다고 합니다(https://sips.synthetix.io/sips/sip-32).

그렇다면 Synthetix가 선택한 방법 외에 front running 공격을 방지할 수 있는 해결책으로는 어떤 것이 있을까요?

이번에는 어팬(aFan)의 사례를 통해 front running 공격과 어팬 팀에서 도입한 해결 방안에 대해 알아보도록 하겠습니다.

어팬(aFan)의 Fanco Swap(Lower Bound)

Fanco Swap은 이미지 기반 SNS 서비스인 어팬(aFan)에서 사용되는 Fanco 토큰에 대한 지급을 보증하기 위해 만들어진 탈중앙화 토큰 간 스왑 컨트랙트입니다. 참고로 HAECHI AUDIT은 어팬 서비스의 Exchange Smart Contract에 대한 스마트 컨트랙트 보안감사를 제공한 바 있습니다.
Fanco Swap에서는 지급 보증을 함과 동시에 시장의 수요와 공급을 적절히 반영하여야 하기 때문에 Fanco의 수요가 많아질수록 Fanco의 가격이 상승하고, Fanco을 지급받았을때 환전하여 받을 수 있는 Ether도 많아지는 구조입니다.

하지만, 지급보증을 해야 하는 만큼, 큰 가격 변동성을 가질 수는 없기에, 해당 부분을 AINetwork팀이 자체적으로 개발한 Payment Guaranteed Polynomial Exchange Rate Scheme (PG-PERS, 논문 링크)통하여 큰 가격 변화 없이 지급 보증을 하도록 구현되어 있습니다.

위의 Synthetix의 사례와 비슷하게, Fanco Swap의 경우 사는 사람과 파는 사람이 매칭되어 결제가 체결되는 구조가 아닌, Fanco Swap의 스마트 컨트랙트와 거래를 하는 구조입니다. 다만, Oracle이 아닌, 수요와 공급으로 인해, 가격 변동이 발생합니다.

하나의 토큰을 Ether로 교환하는 경우만을 봤을때, Ether로 토큰을 구입한 양이 많을 수록 토큰 가격이 증가하고, 토큰을 Ether로 교환한 양이 많을 수록 토큰 가격이 감소합니다.

따라서, 다음 두 함수가 핵심이라고 할 수 있습니다.

tokenToEther(): 토큰을 Ether로 교환

etherToToken(): Ether를 토큰으로 교환

Attack Vector

Synthetix의 사례처럼 한 트레이더가 위 두 함수가 실행 될 것이라는 정보를 블록체인에 기록되기 전에 알게 된다면, 이는 가격변동을 미리 예측하게 되는 것과 마찬가지입니다.

이를 통해 트레이더는 다음과 같은 Front Running이 가능합니다.

  1. user A가 etherToToken() 함수 호출 트랜잭션 발생 (당시 예상 교환 비율은 token: ETH = 100:1)
  2. user B가 etherToToken() 함수 호출 트랜잭션을 더 높은 가격으로 발생
  3. user B의 트랜잭션이 먼저 체결되고, B는 100토큰을 갖게됨.
  4. user B의 트랜잭션이 체결됨에 따라 token:ETH 비율변경(ex. 90:1)
  5. user A의 트랜잭션이 처리되고 A는 90토큰을 갖게됨.
  6. user A의 트랜잭션이 처리됨에 따라 token:ETH 비율변경(ex. 80:1)
  7. user B는 100토큰을 tokenToEther() 함수에 넣어 인출해감
  8. user B의 트랜잭션이 처리되고 B는 1.25ETH를 인출해감

Lower Bound

하지만, Fanco Swap의 Lower Bound 개념으로 인해, A가 토큰 구매시에 기대하는 토큰 양의 최저값을 명시하게 됩니다.

Lower Bound란, etherToToken()/tokenToEther() 함수 실행시 입력값으로, 받고자 하는 토큰/Ether의 최소량을 의미합니다. 만약 이 값보다 적은 양을 받게 되는 경우, 트랜잭션 자체를 취소 하게 됩니다.

위의 공격 시도에서 A가 Lower Bound로 95를 입력 하는 경우, (5)에서 A의 트랜잭션이 취소 되므로, B의 공격 시도 자체가 무산됩니다. 물론 Lower Bound를 도입한다고 해서 Front Running이 불가능 하지는 않습니다. B가 교환비율을 95로 변경되도록 트랜잭션을 변경하여 발생시키게 되면, 여전히 공격 벡터는 존재 합니다만, A의 허용 범위 내에 발생한 것이므로 충분히 예상 가능한 범위에서 피해가 발생할 것입니다. 즉, Front Running 자체를 막는 것은 어렵다는 것을 인지하고, 허용 범위를 명시하는 방식으로 해결한 것이죠.

이러한 시도가 독특한 이유는 또 있습니다. Fanco Swap의 경우 자체적인 로직 구현을 통해 token:ETH비율을 산정하고 있는데, 이 로직이 많은 연산량을 소모합니다.

이더리움 블록체인에서 연산하기에는 너무 많은 연산량이라 이를 줄일 필요가 있었습니다. 따라서 더 많은 연산을 소모하는 etherToToken() 함수에서는 Xt값을 인자로 받습니다. 함수 실행 트랜잭션 생성시의 tokenCirculation을 뜻하는 인자입니다. 이를 통해 실제 트랜잭션 생성 시점에서의 tokenCirculation과 블록체인에 기록된 시점에서의 tokenCirculation이 같은지 확인 한 후, 같다면 최소한의 연산을 통해 블록체인에 기록하는 방식입니다.

HAECHI AUDIT의 스마트 컨트랙트 보안감사를 통해,사용자의 수수료 사용량 최적화와 지급 보증, Front Running 공격까지 대응하는 방법으로 스마트 컨트랙트가 더욱 안전하게 작성될 수 있었습니다😉.

마무리

외부의 값을 가져오는 스마트 컨트랙트나, 여러 사용자가 동시에 작동시킬 수 있는 값을 가지고 있는 스마트 컨트랙트에서는 Front Running 공격을 피할 수 없습니다. 해당 공격에 대응 하는 방법은 Synthetix처럼 또 다른 Front Running이나 Lock을 도입 할 수도 있지만, fanco swap처럼 Front Running 공격을 피할 수 없다는 것을 인지하고 피해 범위를 최소화 하는 것도 가능 할 것입니다.

어떤 방식이 옳은 방식이라고 단정지을 수는 없지만, 프로젝트의 특성에 맞는 보완 방식을 택하는 것이 중요합니다. :)

HAECHI AUDIT은 안전한 스마트 컨트랙트 개발을 위해 스마트 컨트랙트 보안감사 및 개발 서비스를 제공합니다. 또한, 서비스와 블록체인의 연동을 돕는 솔루션을 제공하여 DApp 팀들이 겪는 기술적인 어려움을 해소하는데 도움을 드리고 있습니다.

HAECHI AUDIT 팀에게 질문이 있으시거나, 스마트 컨트랙트 보안감사 및 개발 문의를 하시고자 하는 분은 언제든지 audit@haechi.io로 연락주시기 바랍니다.

감사합니다.

HAECHI AUDIT Official website: https://audit.haechi.io/

HAECHI AUDIT Twitter: https://twitter.com/haechi_audit

Reference

--

--