Search

Claim 가능한 예상시간을 UI로 보여주기

문제 상황

배경지식 설명

Supernova에 스테이킹할 수 있는 자산은 ATOM, OSMO, JUNO 등이 있지만, 편의상 ATOM이라고 칭하겠습니다. 그리고 이 ATOM의 쉐도우 토큰은 snATOM입니다.
[Lazy Minting 기술]
Lazy Minting 기술을 제대로 이해하기 위해서는 Supernova 작동 방식, Oracle(오라클), snATOM 등에 대해 이해하시면 좋을 것 같습니다.
Supernova는 다른 Liquid Staking DeFi와 다르게 ATOM을 스테이킹하면, 쉐도우 토큰인 snATOM을 바로 받을 수 없습니다. 특정 시간 이후에 snATOM을 받을 수(= claim) 있는데, 그 이유는 Supernova의 “Lazy Minting” 기술 때문입니다.
위의 그림에서 1번 시점과 2번 시점에 스테이킹을 하면, Oracle 3일 때 snATOM 발행이 가능하게 됩니다. snATOM은 스테이킹된 ATOM 풀의 지분을 의미합니다.
만약 Oracle 2와 Oracle 3 사이에 스테이킹을 1번만 했다면, 1번 시점에 받을 수 있는 지분이 10% 라고 가정합시다. 그렇게 되면, 1번은 10% 지분에 해당하는 snATOM을 받게 됩니다.
하지만, Oracle 2와 Oracle 3 사이에 스테이킹을 1번 뿐만 아니라, 2번도 함께 했다고 생각해봅시다. 이렇게 되면, 2번이 스테이킹한 ATOM의 양 또한 전체 스테이킹 풀에 포함되어 1번 지분을 계산하기 때문에 10%가 아닌 9%(편의상 9%로 하겠습니다)로 줄어들게 됩니다.
분명 1번은 2번보다 먼저 자신의 소중한 자산을 스테이킹을 함으로써 2번보다 리스크를 더 가져갔음에도 오히려 상대적인 손해를 입게 됩니다. 이는 불공정한 일이라고 할 수 있겠습니다.
따라서, 이러한 1번과 같은 기존 스테이커들의 불공정한 손해를 막고자 snATOM의 발행 주기를 의도적으로 지연시킨 방법이 “Lazy Minting” 방법입니다.
이를 통해 불공정함을 최소화할 수는 있으나, 사용자 입장에서 snATOM을 바로 받지 못하는 불편함을 초래하게 되었습니다.

문제 개요

위의 Lazy Minting 기술 덕분에 불공정함을 최소화할 수는 있었으나, 사용자 입장에서 snATOM을 바로 claim을 하지 못하는 불편함이 발생했습니다.
사용자는 자신이 스테이킹한 ATOM의 대가로 받는 snATOM을 언제 받을 수 있는지(= claim) 알 수 없었습니다. 이로 인해 소위 ‘돈 먹은 것 아닌가?’와 같은 불안감을 사용자에게 갖게 했습니다.
이러한 불안감을 최소화하고자 “Claim 가능한 예상 시간”을 UI에 보여주는 작업을 진행했습니다.

해결 과정

어떻게 구현할까?

구현하기에 앞서서 어떤 것이 필요할지 먼저 꼼꼼하게 관련 리서치를 진행했습니다. 그리고 어떤 것들이 필요할 지 간단히 정리를 했습니다.
1) snATOM이 Claim 가능한 시간 2) Claim 가능한 시간을 위해 필요한 API들 3) React Query 활용해서 커스텀 훅 개발
구현에 필요한 설계를 어느 정도 마무리한 후, 블록체인 코어 개발자께 찾아가 피드백을 구하고 컨펌을 받은 뒤, 작업을 시작했습니다.

snATOM이 Claim 가능한 시간

일정 주기마다 Oracle 봇이 해당 주기 안에 스테이킹한 ATOM을 모아서 상대 체인인 Cosmos에 Delegate(위임)할 때, snATOM 발행이 가능해집니다. Oracle이 Delegate Tx을 발생시키게 되면, 블록에 기록되기 때문에 저는 실제 Claim 가능한 시간을 알기 위해서는 해당 블록에 기록된 시간을 가져오면 되는 것입니다.
즉, 1번 시점과 2번 시점에 스테이킹을 하면 Oracle 3이 Delegate Tx을 발생시킨 시간을 구하면 되고 3번 시점에 스테이킹을 하면 Oracle 4가 Delegate Tx을 발생시킨 시간을 구하면 됩니다. 즉, “현재 Oracle 버전이 Delegate Tx을 발생시킨 시간”이 곧 실제 Claim이 된 시간입니다.
실제 Claim이 되는 시간은 이제 알겠는데, 제가 사용자들에게 보여주고 싶은 것은 Claim이 될 시간입니다.
즉, 1번이 스테이킹을 한 1번 시점에서 1번 사용자에게 “너 [언제] Claim이 가능할 예정이야. 조금만 기다려줘.”에서 [언제]를 보여줘야 합니다.
이 [언제]를 어떻게 보여줄 수 있을까요? 저는 이 부분에 대해서 많은 고민을 했습니다. 그리고 정답을 찾았습니다.
바로, “현재 Oracle보다 바로 전 Oracle이 Delegate Tx을 발생시킨 시간을 구한 뒤, Oracle 주기를 더하자”입니다.
1번 유저가 스테이킹한 시점은 Oracle 3이 아직 Delegate Tx을 발생시키지 않은 시점입니다. 그리고 Oracle 2는 Delegate Tx을 이미 발생시켰습니다. 그러니까 Oracle 2가 Delegate Tx을 발생시킨 시간을 구하고 여기에 Oracle 주기를 더하게 되면, 대략적으로 Oracle 3가 Delegate Tx을 발생시킬 시간과 비슷하게 됩니다.

Claim 가능한 시간을 위해 필요한 API들

이제 1) 실제 Claim 시간(현재 Oracle의 Tx 발생 시간)과 2) 예상 Claim 시간(전 Oracle의 Tx 발생 시간 + Oracle 주기)을 구할 수 있는 이론적인 이해는 완료가 되었습니다.
이를 코드로 구현하기 위해서 필요한 API들은 다음과 같습니다. (편의상 Index를 붙였습니다)
Index
URL
Return 값
API 설명
1
/nova/gal/v1/delegate_version/${zoneID}
현재 Oracle 버전
현재 Oracle 버전을 보여주는 API
2
/nova/gal/v1/delegate_version/${zoneID}/${version}
블록 높이
오라클 버전이 delegate tx을 발생시킨 블록 상태 보여주는 API
3
/blocks/${height}
Tx 발생 시간
블록 높이를 통해 해당 블록에 적힌 기록들을 보여주는 API
1) 실제 Claim 시간 구하는 방법
1번 API로 현재 Oracle 버전을 구하고 ⇒ 이 버전을 2번 API에 사용해서 블록 높이를 구합니다 ⇒ 그 블록 높이로 3번 API에 사용해서 Tx 발생 시간을 구합니다.
2) 예상 Claim 시간 구하는 방법
1번 API로 현재 Oracle 버전을 구하고 -1을 해줌으로써 이전 Oracle 버전을 구한다 ⇒ 이전 Oracle 버전을 2번 API에 사용해서 블록 높이를 구하고 ⇒ 그 블록 높이로 3번 API에 사용해서 Tx 발생 시간을 구합니다. 이 시간에 Oracle 주기를 더해줍니다.

React Query 활용해서 커스텀 훅 개발

React Query의 enabled 라는 메소드를 활용해서 의존성을 갖는 API들을 순차적으로 호출할 수 있었습니다.
[현재 Oracle 버전을 구하는 fetch 함수]
export const fetch_현재_오라클_버전 = async ( zoneID: string, ): Promise<string | null> => { const fetchResult = await fetch( `${REST_BASE_URL}/nova/gal/v1/delegate_version/${zoneID}`, ); const data = await fetchResult.json(); const { version: 현재_오라클_버전 } = data; return 현재_오라클_버전; };
TypeScript
복사
[Oracle 버전으로 블록 높이와 Tx 발생 시간을 구하는 커스텀 훅]
export const use_오라클_버전으로_블록_정보_받기 = ( 오라클_버전: number ) => { const { data: 블록_정보 } = useQuery({ queryKey: ["delegateBotInfo", 오라클_버전], queryFn: () => fetch_블록_정보_구하기(오라클_버전), enabled: !!오라클_버전 }); const { data: 블록_시간 } = useQuery({ queryKey: ["blockTime", 오라클_버전], queryFn: () => fetch_블록_시간_구하기(블록_정보?.높이 || "0"), enabled: !!블록_정보 && !!(블록_정보?.상태 === BOT_SUCCESS_STATE), }); return { data: { 블록_시간, 블록_정보?.높이 } }; };
TypeScript
복사
[실제 Claim된 시간을 구하는 커스텀 훅]
export const use_실제_claim된_시간_받기 = (zoneID: string) => { const { data: 현재_오라클_버전 } = useQuery({ queryKey: ["currentDelegateBotVersion", zoneID], queryFn: () => fetch_현재_오라클_버전(zoneID), enabled: !!zoneID, }); const { data: 블록_시간 } = use_오라클_버전으로_블록_정보_받기( 현재_오라클_버전, ); const 실제_claim된_시간 = 블록_시간.format(DATE_FORMAT) return { data: 실제_claim된_시간 }; };
TypeScript
복사
[예상 Claim 시간을 구하는 커스텀 훅]
export const use_예상_claim될_시간_받기 = (zoneID: string) => { const { data: 현재_오라클_버전, } = useQuery({ queryKey: ["currentDelegateBotVersion", zoneID], queryFn: () => fetch_현재_오라클_버전(zoneID), enabled: !!zoneID, }); const 이전_오라클_버전 = 현재_오라클_버전 - 1; const { data: 블록_시간 } = use_오라클_버전으로_블록_정보_받기( 현재_오라클_버전, ); const 예상_claim될_시간 = 블록_시간.format(DATE_FORMAT) + 오라클_주기 return { data: 예상_claim될_시간 }; };
TypeScript
복사
1) 실제 Claim된 시간을 구하는 커스텀 훅과 2) 예상 Claim 시간을 구하는 커스텀 훅을 구현했습니다! 이 두 훅을 UI에 적절하게 잘 보여주면 됩니다.

해결 결과

위의 두 커스텀 훅 및 여러 fetch 함수 등을 통해 사용자에게 UI에서 Claim 가능한 예상 시간을 보여줄 수 있게 되었습니다. 아쉽게도 이 UI가 적용된 버전은 배포가 되지 않은 상태로 프로젝트가 종료되어 직접적인 사용자의 피드백은 들을 수는 없었습니다.
하지만, 이번 작업을 통해 조금이나마 불편함을 줄여줬다는 사내 피드백을 받아서 뿌듯했습니다.

아쉬웠던 점

사실 Claim 예상 시간을 보여주는 방법은 개인적으로 근본적인 해결책이 아니라 미봉책이라고 생각했습니다.
Lazy Minting은 DeFi 최초로 기존 스테이커들의 불공정함을 잡아주는 방식이어서 의미가 꽤 있다고 생각합니다. 그리고 저 또한 이 방향에 대해서 공감합니다. 하지만, 사용자 입장에서는 Claim을 바로 하지 못한다는 사이드 이펙트가 공정성 확보보다 더 크게 느껴져 사용자 경험을 크게 저해시킨 것 같습니다.
그렇기에 해당 방식에 대한 실효성에 대해서 한 번 더 재고해야 된다는 의견을 조심스럽게 기획 팀에 전달드렸습니다. 다행스럽게도 기획 팀에서 해당 이슈에 대해서 앞서 인지하고 계셨고 이에 대해서 저와 비슷한 입장을 갖고 계셨습니다.

더 나은 방법 고민

Query를 주기적으로 하는데, 생각보다 불필요한 Query가 있었다. 이를 조금이라도 줄이기 위해서 React Query의 cache를 이용했으면 괜찮았으려나? (블록체인 네트워크의 안정성이 높아질 수 있었을 것 같다)