‘글로벌 DeFi 해커톤’ 참여기 : 단일 토큰 스테이킹 설계 및 관련 보안 이슈

이번 글은 TAEBIT에서 주관한 글로벌 DeFi 해커톤[1]에 대한 소개와 소감을 적은 지난 글에 이어서, 저희 BLEEP 팀이 해커톤에서 구체적으로 어떤 프로젝트를 수행하였는지 소개하고자 합니다. 대회에서 저희 BLEEP 팀은 지난 글에서 짧게 소개되었던 것처럼 단일 토큰 스테이킹 시스템을 구현했습니다. 저희가 구현한 단일 토큰 스테이킹에 대하여 설명하기 이전에 해당 시스템을 이해하기 위한 배경지식을 설명하고, 저희가 구현한 서버와 스마트 컨트랙트의 순으로 소개하고자 합니다.

Uniswap AMM 매커니즘

중앙화 된 거래소나 기존 탈 중앙 거래소(Decentralized Exchange, DEX) 시스템과 다르게, Uniswap[2]은 사용자 간의 거래를 수행하기 위하여 모든 사용자의 매수 및 매도 주문을 기록한 오더북을 사용하지 않고, 자동화된 시장 메이커(Automated Market Maker, AMM)[3]를 사용합니다. 구체적으로 Uniswap은 AMM중 Constant Product Market Maker(CPMM)라는 알고리즘을 사용하는데, 이는 두 토큰 X, Y에 대하여 다음 식을 만족하도록 교환을 수행하는 알고리즘입니다.

(시스템 내 X의 수량×시스템 내 Y의 수량)=K (K는 고정값)

예를 들어, CPMM을 사용하는 DEX 시스템 내부에 유니스왑 토큰(UNI)이 10, 다이 토큰(DAI)이 50,000만큼 존재하는 경우, K값은 다음과 같습니다.

(1): K=10×50,000=500,000

토큰스왑 (Token Swap)

사용자가 DEX시스템을 이용하여 암호화폐를 다른 암호화폐로 환전하는 것을 토큰 스왑(Swap) 이라고 합니다. 사용자가 하나의 암호화폐를 스왑시 사용자가 제공한 암호화폐는 DEX 시스템에 추가가 되고, 스왑이전과 이후 K값이 동일해지도록 토큰쌍의 상대 토큰이 유저의 지갑으로 전달되게 됩니다. 예를 들어, 어떤 사용자가 수식 (1)의 예시에서 1UNI를 DAI로 교환하고자 할 때, 받는 DAI는 다음과 같이 계산됩니다.

시스템 내 UNI의 수량=10+추가된 UNI=11

목표 DAI 수량=K/(시스템 내 X의 수량)=500,000/11=45,454.54

사용자가 받을 DAI 수량=기존 DAI 개수-목표 DAI 수량=50,000-45,454.54=4,545.45

따라서, 사용자는 위 예시에서 4,545.45만큼의 DAI를 받게 됩니다.
위와 같은 방식으로 AMM 알고리즘을 사용함으로써 사용자는 시스템 내 현재의 교환 비율에 맞게 토큰 교환을 수행할 수 있습니다. AMM을 이용하여 토큰 간의 거래를 수행하는 경우, 언제든지 즉시 교환이 이루어진다는 이점을 가집니다.

토큰쌍 스테이킹 (Token Pair Staking)

알고리즘의 특성상 토큰 스왑시 교환할 대상 토큰이 시스템 내에 충분히 존재하지 않으면 토큰 스왑을 수행할 수 없습니다. 또한, 시스템 내 각 토큰의 수량이 적다면 토큰 교환시마다 토큰 교환 비율이 급격하게 변하게 됩니다. 토큰 교환 비율은 최대한 많은 양의 토큰이 시스템 내부에 쌓여 있을수록 안정적인데, 토큰을 교환 비율에 맞춰 시스템 내부에 쌓아 놓는 것을 토큰 스테이킹이라고 합니다. Uniswap 시스템은 사용자들이 토큰 스테이킹을 수행하도록 장려하기 위하여 토큰 스왑시 수수료를 추가하고, 수수료의 일부를 토큰을 스테이킹한 사용자가 가져갈 수 있도록 구현되어 있습니다.

단일 토큰 스테이킹 구현

Uniswap의 토큰 스테이킹은 토큰 교환 비율에 영향을 주지 않게 토큰을 DEX 시스템에 추가하기 위하여 토큰 교환 비율에 맞추어 토큰쌍을 추가해야 하기 때문에 두가지 토큰을 미리 준비해놓아야 한다는 불편함이 있고, 기존 예치 시스템과의 차이점 때문에 일반 사용자들이 사용에 어려움을 느끼게 되는 요인 중 하나입니다. 저희 팀은 토큰을 스테이킹하려는 사용자가 토큰쌍을 구성할 필요 없이 하나의 토큰을 이용하여 스테이킹 하는 시스템을 구현하기 위하여 1) 하나의 서버에서 여러 유저들로부터 온 단일 토큰 스테이킹 요청을 묶어 토큰쌍을 구성하는 방법을 사용하였습니다. 이후, 2) 구성된 요청 그룹을 처리하는 스마트 컨트랙트 코드를 제작하여 단일 토큰 스테이킹을 구현했습니다. 이후 글에서는 각 단계별로 어떻게 시스템을 만들었는지 보다 구체적으로 설명드리겠습니다.

Step 1: Server Request Queueing
저희는 단일 토큰 스테이킹을 위한 단일 토큰 스테이킹 요청을 수합하여 하나의 트랜잭션으로 구성하기 위하여 NodeJS 서버를 작성하였습니다. 서버는 유저로부터 다음 메세지를 받습니다.

요청 데이터:[유저 지갑주소,대상 토큰명,토큰 액수,토큰쌍을 구성할 상대 토큰명]
요청 메세지:[요청데이터,요청데이터 해시값에 대한 서명]

그림 1 서버 내 요청 처리 큐

서버는 요청 메시지가 사용자 클라이언트로부터 도착한 후, 유저로부터 도착한 요청 메시지가 유저의 지갑을 이용하여 서명한 것인지 확인합니다. 서명이 맞는 경우, 관련된 토큰쌍을 관리하는 서버의 큐에 해당 요청을 추가합니다. 큐 안의 데이터는 토큰 스테이킹 요청의 대상 지갑주소, 스테이킹 할 토큰 타입, 토큰 수량 등의 정보를 포함하고 있습니다.

위 예시는 토큰 A, B로 구성된 토큰쌍에 대한 요청을 관리하는 큐로, w1에서 w4에 해당하는 지갑이 A또는 B 토큰에 대한 단일 스테이킹을 요청하였을 때의 A-B 토큰쌍을 처리하는 큐의 예시입니다. 요청 메시지가 들어올 때마다 서버는 큐의 tail 부분에 요청 데이터를 추가하게 됩니다.

서버는 일정 주기마다 토큰쌍의 교환 비율에 맞추어 적당한 양의 요청이 큐에 존재하는지 확인하며, 충분한 양의 요청이 큐에 존재하는 경우, 요청을 들어온 순서대로 뽑아 해당 요청들을 묶어 그림과 같이 스마트 컨트랙트 함수를 수행하기 위한 데이터로 변환합니다.

그림 2 처리할 단일 토큰 스테이킹 요청에 대한 그룹화

마지막으로, 각 유저가 해당 데이터에 대하여 동의 했음을 스마트 컨트랙트가 알도록 하기 위해서는 각 유저의 해당 데이터의 해시값에 대한 서명이 필요합니다. 이를 위하여 유저 클라이언트들에게 트랜잭션을 위하여 묶인 데이터를 전송하고, 유저 클라이언트들은 해당 데이터에 동의했다는 표시로 데이터의 해시값에 대한 서명을 생성하여 서버에게 전송합니다. 참여하는 모든 유저의 서명이 모이면 서버는 그림의 예시와 같이 합의된 데이터와 참여 유저들의 서명을 입력값으로 다음 순서에 설명할 스마트 컨트랙트의 토큰 스테이킹 함수를 실행합니다.

그림 3 완성된 요청 그룹에 대한 토큰 스테이킹 트랜잭션 전송

Step 2: DeFi Smart contract
서버가 여러 유저들의 토큰 스테이킹 요청을 묶어 스마트 컨트랙트의 토큰 스테이킹 함수를 실행하면 다음을 과정을 수행합니다.

  1. 토큰 스테이킹 요청 보낸 유저들의 지갑에서 각 유저들이 요청한 액수만큼의 토큰을 서버의 지갑으로 이동
  2. 스테이킹 대상이 되는 토큰쌍에 대하여 서버의 지갑에 전송된 토큰들을 이용하여 기존의 Uniswap 토큰 스테이킹 수행
  3. 서버의 지갑에 토큰 스테이킹의 결과로 들어온 유동성 토큰을 각 유저들의 토큰 스테이킹 요청 액수에 비례하여 각 유저들의 지갑으로 분배

해당 과정을 수행함으로써, 각 유저들은 단일 토큰에 대한 토큰 스테이킹을 수행하면서 기존의 토큰 스테이킹과 동일한 효과를 보이도록 설계할 수 있습니다.

저희가 해당 프로젝트에서 스마트 컨트랙트 코드를 작성할 때 고려한 사항은 두 가지가 있습니다. 하나는 서버가 유저들이 보낸 요청을 다른 트랜잭션에도 추가하여 유저가 스테이킹에 사용하려는 토큰 개수 이상의 토큰을 처리할 수 있다는 점입니다. 예를 들어, 유저가 하나의 요청만 보냈음에도 서버는 그림과 같이 해당 요청을 다른 요청들과 묶어서 여러 번 토큰 스테이킹이 수행되게끔 공격할 여지가 있습니다.

그림 4 서버의 요청 이중처리 문제

이를 방지하기 위해서 스마트 컨트랙트에서 토큰 스테이킹을 진행하기 이전에 해당 요청 그룹에 대하여 유저가 서명 데이터가 존재하는지 확인하는 과정을 추가했습니다.

또한, 여러 유저의 서명을 스마트 컨트랙트 레벨에서 확인하는 코드를 작성할 때 동일한 데이터를 두 번 이상 보내서 공격을 수행하는 리플레이 공격이 발생할 수 있다는 점을 고려하여 코드를 작성했습니다. 이더리움에서는 두 명 이상의 유저의 서명(Multisig)을 트랜잭션 처리 레벨에서 확인하는 Multisig 트랜잭션에 대한 지원을 하지 않기 때문에 스마트 컨트랙트 레벨에서 Multisig 기능을 구현해야 합니다. 이 경우, 이더리움 트랜잭션의 nonce 값을 이용한 리플레이 공격에 대한 방어 방법은 스마트 컨트랙트 함수 내부에 구현된 Multisig 기능에 대한 리플레이 공격을 막을 수 없습니다. 스마트 컨트랙트 함수 로직에 대한 리플레이 공격을 막기 위하여 저희는 다음과 같이 코드를 작성하였습니다.

  1. 처리 대상이 되는 스테이킹 요청 그룹 뒤에 서버의 지갑 주소 및 nonce값을 추가. nonce값은 임의의 숫자로, 서버가 매번 요청을 처리할 때 마다 1씩 증가
  2. 각 유저가 스테이킹 요청 그룹에 대하여 서명하는 대신 해당 그룹에 서버의 지갑 주소 및 nonce값을 포함한 데이터에 서명을 수행함
  3. 스마트 컨트랙트 함수에서 입력값으로 주어진 nonce값이 스마트 컨트랙트에 기록되어있던 과거 서버의 nonce값보다 큰 경우에만 처리를 진행함. 이후 스마트 컨트랙트에 기록되는 서버의 nonce 값을 입력값으로 주어진 nonce 값으로 업데이트

위와 같이 다양한 보안 위험성에 대한 고려 사항들을 반영하여 스마트 컨트랙트 처리 과정에서 발생 가능한 보안 취약점을 방어하면서 단일 토큰 스테이킹을 수행하는 시스템을 개발할 수 있었습니다.

결론

본 대회에서 저희 팀은 Uniswap을 기반으로 한 DeFi 시스템에서 단일 토큰에 대한 스테이킹을 수행하는 방법에 대해서 고안하고 해당 방법에서 발생 가능한 보안 문제를에 대하여 처리하는 과정을 생각하였습니다. 프로토타입을 구현하여 시험 해 본 결과 단일 토큰 스테이킹을 문제없이 수행함과 동시에 요청에 대한 Batch효과로 트랜잭션에 사용된 가스비용 또한 절감할 수 있음을 확인 하였습니다.

글을 마치며

지금까지 저희 BLEEP팀이 TAEBIT에서 주최한 글로벌 DeFi 해커톤에 참가하면서 어떠한 프로젝트를 수행하였고, 스마트컨트랙트 개발 특성상 어떠한 사항을 고려해야하는지에 대하여 간단히 살펴보았습니다. 스마트 컨트랙트는 분산화된 블록체인 네트워크에서 동작이 수행되는 것과 같은 특수한 상황으로 인하여 기존 어플리케이션과는 다른 보안문제를 고려해야 합니다. 이 글을 통해서 많은 분들이 스마트 컨트랙트 개발에서 고려할 보안 문제에 관심을 가지는 계기가 되었으면 좋겠습니다.

※ 본 대회 참여는 IITP 사업 (과제명 : N01210015 블록체인 에뮬레이션을 위한 모듈형 라이브러리 및 엔진 기술 개발, 2020~2021년) 수행의 일환으로 참여하였음.

[1] 글로벌 DeFi 해커톤 (https://taebit.com/hackathon)
[2] Uniswap (https://uniswap.org)
[3] AMM (https://coinmarketcap.com/alexandria/article/what-are-automated-market-makers)

현진 김

김현진 연구원은 한국과학기술원에서 2020년 석사를 마치고 2020년부터 카이스트 사이버보안연구센터 연구1실 연구원으로서 블록체인 에뮬레이션 및 평가 플랫폼 등 블록체인 시스템 및 보안과 관련된 다양한 연구를 수행하고 있다. Keywords: Blockchain, Distributed Network Simulation, Smart Contract Security

2 명이 이 글에 공감합니다.