DAO Service
반년정도 진행된 DAO 프로젝트가 초기 MVP모델이 최근에 완성되었다.
나뿐만 아니라 개발팀전원 web3 기반지식이 부족한 상태에서 애자일방식으로 개발을 진행되었다.
각자의 진행속도와 이해도가 너무나도 천차만별이라 개발하면서 스트레스도 많이 받고, 너무너무 힘들었다.
그중 애를 많이 먹였던 스왑과정에 대해 공유해보려한다.
문제의 이해를 돕기 위해 Flow를 설명하자면 아래 그림과 같다.
아래 예시는 토큰예치나 스왑등의 모든 트랜잭션의 Flow는 동일하다.
최종 DB는 transaction receipt 발급 여부를 기준으로 update하는데,
특히 스왑과 관련된 트랜잭션이 처리가 까다로웠다.
스왑(Token Transfer)에 대한 결과는 어디에 있을까?
클라이언트에서 서명 후 블록체인에 전송된 트랜잭션이 실제 블록에 생성되기까지는 랜덤한 시간이 걸린다.
(대략20초이내 소요되지만, 한국시간기준 5시를 넘어가면 가스비와 처리시간이 급격히 늘어난다(최대 2분이상))
물론 일정시간이 지나면 abort 되지만,
정상적으로 처리된 트랜잭션의 정보는 receipt을 통해 대부분의 정보확인이 가능하다.
const receipt = await web3.eth.getTransactionReceipt(transactionHash);
대부분의 트랜잭션 데이터는
web3 인스턴스에서 제공되는 getTransactionReceipt 메서드를 이용해 블록에 생성된 트랜잭션의 정보를 통해 얻고 있다.
그러나, 스왑거래한정해서
필요로하는 정보인 1. 유저가 보낸 토큰의 종류와 양과 2. 교환된 토큰의 종류와 양이 receipt에 들어있지 않아 처리에 어려움을 겪었다.
1. Etherscan api를 통해 접근(Fail)
etherscan api에서는 트랜잭션해시를 제공하면 관련 데이터를 확인할 수 있다.
하지만 필요로하는 토큰간의 교환정보는 없었다. input key부분을 디코딩해도
애초에 넣었던 파라미터 정보(내가 보낸 토큰 유형과 양)를 확인할 뿐
어떤 토큰을 얼마를 받았는지는 알 수 없었다.
const apiUrl = `https://api-goerli.etherscan.io/api?module=proxy&action=eth_getTransactionByHash&txhash=${txhash}&apikey=${process.env.ETHERSCAN_API_KEY}`;
const response = await axios.get(apiUrl);
console.log(response.data);
//return
{
blockHash: '0x3bc7adb12235d2f02d2512878097706746ca9ddc455740f97adacb2579d4ae7d',
blockNumber: '0x8d1c00',
from: '0x75679d406187490ef00195b4d4089859e7ec2be2',
gas: '0x38cb2',
gasPrice: '0x609f23',
hash: '0xd34ed2a1b777a9ad289066c5dc53b31ceae8bb0f3fb2e62bea75f2db53657d86',
input: '0x7bbd182a000000000000000000000000000000000000000000000000002386f26fc10000000000000000000000000000326c977e6efc84e512bb9c30f76e30c160ed06fb',
nonce: '0x54',
to: '0xffd2649058ba61225ded2c3dd118eb445fe88229',
transactionIndex: '0x34',
value: '0x0',
type: '0x0',
chainId: '0x5',
v: '0x2d',
r: '0x494c173a50d0502d12052b2ac26d9706c65349f02302ffbb91fbed68445b4054',
s: '0x1a66a86df323e4d1edbbb7de165b7178438dca3b4b6939b124721e4629300e61'
}
2. getPastEvents 메서드를 통한 접근(fail)
'getPastEvents'메서드를 과거이벤트를 가져와서 조회하거나 분석할 수 있다.
하지만 여기도 마찬가지로 내가 알고 싶은 값은 들어있지 않다.
const events = await contract.getPastEvents('allEvents', {
fromBlock: 0,
toBlock: 'latest',
});
// console.log('이벤트 로그:', events);
//return
{
address: '0xFfD2649058BA61225DeD2C3dD118Eb445Fe88229',
blockNumber: 9247744,
transactionHash: '0xd34ed2a1b777a9ad289066c5dc53b31ceae8bb0f3fb2e62bea75f2db53657d86',
transactionIndex: 52,
blockHash: '0x3bc7adb12235d2f02d2512878097706746ca9ddc455740f97adacb2579d4ae7d',
logIndex: 3179,
removed: false,
id: 'log_2bf77962',
returnValues: Result {
'0': '10000000000000000',
'1': '0x326C977E6efc84E512bB9C30f76E30c160eD06FB',
ethAmount: '10000000000000000',
tokenOut: '0x326C977E6efc84E512bB9C30f76E30c160eD06FB'
},
event: 'SwapEthToERC20',
signature: '0x8379d1b65d7611055492253dd1aa4fd7936dc09149990a4263adde6275f8b416',
raw: {
data: '0x000000000000000000000000000000000000000000000000002386f26fc10000000000000000000000000000326c977e6efc84e512bb9c30f76e30c160ed06fb',
topics: [Array]
}
}
3. Etherscan api (Token Transfer List) (half success..)
Accounts - Etherscan
...
docs.etherscan.io
etherscan에서 제공하는 transfer api를 통해 transfer pair로그를 찾아낼 수 있었다.
이대로 끝난다면 너무나 좋겠지만 생각하지 못한 이슈들로 곳통을 주었다..
transfer pair로그에는 모든 스왑케이스가 남지 않을 수 있다.
1. 불완전한 로그
스왑의 경우 3가지 케이스가 있다.
1. 이더를 토큰으로
2. 토큰을 이더로
3. 토큰을 토큰으로
여기서 ether의 경우 다른 ERC-20 토큰과 이더를 교환할 수 있도록
WETH(Wrap Ethereum)로 교환이 된다.
그런데 로그에 교환쌍 중에 WETH에 대한 내역은 빠진 채로 로그에 남는 경우가
빈번하게 발생했다.(goerli 테스트넷이라 그런가).
2. 동시성 제어 문제
스왑 트랜잭션이 정상적으로 수행되고 블록이 생성되었을 경우
컨트랙트에 스왑내용이 업데이트되는 시점보다
etherscan api로 transfer의 로그를 찾는 시점이 늦었다.
즉, 컨트랙트에 이미 반영되었지만 이더스캔에서는 아직 확인이 되지 않는 상황이 발생한다.
이미 반영되었는데 로그를 못 찾았다고 계속 기다리는 건 ux에 좋지 않은 상황.
컨트랙트에 로그를 남기는 코드를 추가해 볼까 생각했다.
하지만, 클라이언트 정책에 따라 가변적인 로그를 반영하자고 컨트랙트를 수정할 경우
트랙잭션 연산량에 따라 결정되는 가스비에도 영향이 있고, 수정될 때마다 새로 배포해야 하기 때문에
생산성, 유지보수, 가스비등 모든 측면에서 좋을 게 없어 보였다.
결국
어차피 db는 이전 컨트랙트의 스냅숏형태로 보관되고 있기에
이전 컨트랙트정보와 컨트랙트가 보유한 자산내역을 비교한 차액만큼 로그로 남기기로 했다.
문제는 해결했으나, 아직 두 가지 문제가 존재한다.
첫쨰로는
점검 중이거나 정상적으로 작동되지 않는 경우 서비스제공에 문제가 있기에
교환관련한 내용 으를 이더스캔에서 확인하는 의존성코드의 개선여지가 필요하다.
두 번째는 다량의 트랜잭션이 발생할 경우 DB의 무결성 확보가 힘들다는 점이다.
다량의 트랜잭션발생 시 연계된 트랜잭션이 처리중일경우 동시성제어의 문제점도 존재하므로
개선의 여지가 필요하다.
추가) 가속화이슈
메타마스크에서는 트랜잭션 서명전송과정 이후 추가적인 행동이 존재한다
트랜잭션풀에서 채굴자에 의해 블록에 옮겨지기까지 트랜잭션풀에 머물게 되는데 가스비에 따라 소요시간이 다르다
이때 같은 nonce값으로 가스비를 추가로 얹을 경우
기존트랜잭션거래는 소멸되고 새로 전송한 트랜잭션(높은 가스비)이 블록에 생성될 준비를 한다.
이로 인해 현재 설계구조상 문제가 발생했고 클라이언트와 폴링로직을통해 해결했다.
- 폴링에 가장 중요한 역할을 하는 트랜잭션해시.
- 트랜잭션해시를 기준으로 transactionReceipt을 확인한다.
- 하지만 “메타마스크”에서 가속화할 경우 새로운 트랜잭션해시가 발생되어 기존에 가지고 있던 트랜잭션해시는 쓸모가 없어진다.
- 클라이언트에서 바로 블록체인으로 sending 하는 구조가 아니고 서버가 deliver역할을 해야 하는 구조상 트랜잭션해시를 다시 받아 이더스캔에 폴링 하는 대기열에 다시 토스시켜줘야 함
- 트랜잭션해시를 받아서 다시 폴링 해야 한다.
- 클라이언트에서는 wagmi라이브러리에는 useForWaitTransaction메서드는 가속화할 경우 대체해시를 받을 수 있다
- 가속화해시는 폴링, 이전해시값은 소멸시킴
https://wagmi.sh/react/hooks/useWaitForTransaction#onreplaced-optional
useWaitForTransaction
React Hook for declaratively waiting until transaction is processed. Pairs well with useContractWrite and useSendTransaction.
wagmi.sh
트랜잭션취소이슈)
가속화 옵션과 더불어 트랜잭션취소가 있다.
이경우 기존 트랜잭션은 소멸되고 블록생성을 위해 가용될 예정인 가스비가 리턴된다.
마찬가지로 트랜잭션 receipt이 발급되어 마치 정상적으로 트랜잭션이 블록에 생성된 것으로 처리될 수 있기에
이경우에는 트랜잭션을 일으킨 주소와 과 to 주소가 같으므로 별도의 예외처리를 진행해 주었다.
'블록체인' 카테고리의 다른 글
[DID] DID개발을 위한 DID, DID Document 개념정리 (0) | 2023.09.26 |
---|---|
블록체인 금융 산술데이터처리(부동소수점연산) (0) | 2023.08.22 |