오늘은 Swap Trx이 완료된 후 실제 얼마의 수량이 Swap 되었는지 사용자에게 알려주기 위해
Web3.js를 사용해 해당 값을 확인하는 방법에 대해 포스팅하려 한다.
아래와 같이 사용자가 1개의 ETH를 지불하면 30개 정도의 AAVE를 받을 수 있다고 되어있는데
Pool의 상황이 실시간으로 바뀌기 때문에 실제로 스왑 된 수량은 30개와 다를 수 있다.
따라서 트랜잭션이 완료된 후 Swap된 수량을 조회해야 사용자에게 정확한 수량을 알려줄 수 있다.

우선 Swap된 수량을 알기 위해서는 ABI(Application Binary Interface)에 포함된 Event라는 개념을 알아야 한다.
1. ABI와 Event
프론트에 블록체인을 연결해 본 사람이라면 ABI에 대해서 많이 들어보고 사용해 보았을 것이다.
ABI는 해당 컨트랙트에 대한 청사진(?) 또는 설계도라고 생각하면 될 것이다.
그래서 ABI에는 컨트랙트에서 사용되는 여러 가지 요소들과 해당 파라미터가 기재되어 있는데
일반적으로 아래와 같은 2가지 요소를 포함하며
- Function (함수):
- 함수는 스마트 계약의 외부 호출을 위한 인터페이스를 정의합니다. 이는 계약의 상태를 변경하거나 정보를 반환하는 작업을 수행할 수 있습니다.
- 함수의 이름, 매개변수 타입 및 반환값 타입을 포함하여 외부에서 호출 가능한 메소드를 정의합니다.
- Event (이벤트):
- 이벤트는 스마트 계약에서 발생하는 특정 사건을 나타내며, 외부로 알리기 위해 사용됩니다.
- 일반적으로 계약의 상태 변화나 중요한 사건 발생을 외부에 알리는 데 사용됩니다.
- 이벤트의 이름 및 파라미터 타입을 정의합니다.
추가적으로 Fallback Function (폴백 함수), Constructor (생성자), Modifier (수정자) 같은 것들도 포함한다고 한다.
여하튼 여기서 중요한 것은 이것들 중 Event라는 것을 통해서 실제로 Swap된 수량을 구할 수 있다는 것이다.
2. Event란 무엇일까?
Event는 트랜잭션 처리 중에 발생하는 중요한 사건(수행, 활동)을 기록하는 역할을 하며, 주로 스마트 계약의 상태 변화나 기타 중요한 사항을 외부로 알리기 위해 사용된다.
예를 들면 아래와 같은 솔리디티(Solidity) 코드에서 ItemAdded라는 Event는 addItem이라는 Function이 실행될 때 emit을 통해 함께 수행된다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract EventExample {
// 이벤트 선언
event ItemAdded(address indexed _sender, string _item);
// 상태 변수
string[] public itemList;
// 아이템을 추가하는 함수
function addItem(string memory _item) public {
itemList.push(_item);
// 이벤트 발생
emit ItemAdded(msg.sender, _item);
}
// 아이템 목록을 가져오는 함수
function getItemList() public view returns (string[] memory) {
return itemList;
}
}
이러한 행위는 프론트엔드에서 console.log를 찍는 것과 비슷한 행위로(물론, 완전히 같지는 않음...)
해당 수행내용 중 기록해야 할 것들에 대한 데이터(로그)를 Trx에 기록하는 역할을 한다.
이를 통해 Event를 얼마나 잘 만들고 배치하느냐에 따라 블록체인 활동에 대한 추적이 편리해지고
스마트컨트랙트를 사용한 서비스가 더 스마트(?)해질 수 있음을 알 수 있다.
3. Event는 어떤 형태로 기록을 남기는가
우선 트랜잭션이 완료된 후 Receipt를 열어보면 아래와 같이 생겼다.
const receipt = web3.eth.getTransactionReceipt(hash)
> {
"blockHash": "0x9f9c92f02fe936e1ea223977a7b870e6eec5ceb4f7c10371f82c702d2c306229",
"blockNumber": 37315712,
"contractAddress": null,
"cumulativeGasUsed": 362849,
"effectiveGasPrice": 101000000000,
"from": "0x6d71137cf5fe309b580bfcacf6b05c939128e768",
"gasUsed": 136840,
"logs": [...], <<<< 여기
"logsBloom": "0x00000000000000000000004000000000000000000000000000000000000000000000000000000001000000000000000000040000008000000000000000200000001000000000000000000108000000000000000000000000000000000000000000000000000000000000000100000000000000000000000080010010000000000001000000000000000000000000000000040000000000000000000000000400000000000900000000000000000000000000000000000000000000000000000000000002000001000000000000000000000000200000000000000000000000001000000000000004000000040000000000000000000000000800000000000000",
"status": true,
"to": "0x35fc44895b5cc6cd681d89988e4964b00a7667e5",
"transactionHash": "0xf32c2c800c7cbe421c6abf29065c25133f3011b50f458c87bcf31c2b01a3f46e",
"transactionIndex": 7,
"type": "0x2"
}
여기에서 Event의 기록은 logs에 배열로 담기게 된다.
내가 살펴볼 SwapV3 event abi와 생성된 log를 살펴보면 아래와 같다.
// event abi
const SwapV3_Abi = {
anonymous: false,
inputs: [
{
indexed: true,
internalType: "address",
name: "sender",
type: "address",
},
{
indexed: true,
internalType: "address",
name: "recipient",
type: "address",
},
{
indexed: false,
internalType: "int256",
name: "amount0",
type: "int256",
},
{
indexed: false,
internalType: "int256",
name: "amount1",
type: "int256",
},
...
],
name: "Swap",
type: "event",
};
// Receipt의 log
{
"blockHash": "0x9f9c92f02fe936e1ea223977a7b870e6eec5ceb4f7c10371f82c702d2c306229",
...
"logs": [
{
"address": "0x244c72AB61f11dD44Bfa4AaF11e2EFD89ca789fe",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x00000000000000000000000067a14bd9231822ec7fc65300cea3731cbff8d041",
"0x0000000000000000000000006d71137cf5fe309b580bfcacf6b05c939128e768"
],
"data": "0x000000000000000000000000000000000000000000000000000001499bbd90e1",
"blockNumber": 37315712,
"transactionHash": "0xf32c2c800c7cbe421c6abf29065c25133f3011b50f458c87bcf31c2b01a3f46e",
"transactionIndex": 7,
"blockHash": "0x9f9c92f02fe936e1ea223977a7b870e6eec5ceb4f7c10371f82c702d2c306229",
"logIndex": 0,
"removed": false,
"id": "log_b1d98e38"
},
...
],
...
}
여기서 유심히 봐야 할 값은 topics와 data이다.
- topics의 첫 번째 값(topics[0])은 event의 abi를 해싱한 값으로 어떤 이벤트의 로그인지 확인할 수 있는 지표임
- topics의 나머지 값은 event abi에서 indexed가 true로 설정된 파라미터 값이 해싱된 값임
- 따라서 일반적으로 topics의 길이 = 파라미터(indexed) 개수 + 1로 볼 수 있음
- indexed가 false인 파라미터 값은 해싱되어 data에 기록됨
위 내용을 보면 abi의 파라미터 값이 topics과 data에 나뉘어서 기록된다.
topics에 저장되는 indexed된 파라미터는 데이터 검색의 키워드 같은 역할(인스타의 해시태그# 같은...)을 하기 위해 이곳에 따로 저장된다고 한다. 그리고 data에 indexed: false인 값만 기록하는 이유는 indexed: ture인 것들은 이미 topics에 기록되어 있기 때문에 효율적인 측면에서 중복기록하지 않는 것이라고 한다...
4. Event 디코딩하기
필자는 아래와 같은 방법으로 디코딩을 진행했다.
const results = [];
// 1. event 식별자
const eventSignature =
web3.eth.abi.encodeEventSignature(SwapV3_Abi);
// 2. 해당 event 필터링 및 디코딩
for (const log of receipt.logs) {
if (log.topics[0] === eventSignature) {
const result = web3.eth.abi.decodeLog(
SwapV3_Abi.inputs,
log.data,
log.topics.slice(1),
);
results.push(result);
}
}
// 3. 마지막 Swap Event 선택
const lastEvent = results[results.length - 1];
console.log(lastEvent)
> {
...,
"sender": "0x35fc44895b5cC6Cd681D89988E4964B00a7667e5",
"recipient": "0x6D71137cf5FE309b580BfCAcf6B05c939128E768",
"amount0": "-1404437618223",
"amount1": "1000000000000",
"sqrtPriceX96": "66837432141388463232901347790",
"liquidity": "8904597423505602441712",
"tick": "-3402",
"startTick": "-3402"
}
// 4. pair 중 out된 토큰 수량 추출
const toAmount_wei = BigNumber(lastEvent.amount0).isNegative()
? BigNumber(lastEvent.amount0).abs().toFixed()
: BigNumber(lastEvent.amount1).abs().toFixed();
- 원하는 Event에 대한 log를 찾기 위해 해당 Abi를 Signature로 해싱함
- 해싱한 Signature를 topics[0]과 같은지 비교함 > 같다면 decodeLog로 디코딩을 진행함. 해당 Abi에 포함된 모든 파라미터에 대한 데이터가 필요하기 때문에 log.data와 topics[0]을 제외한 topics.slice(1)이 필요함
- 사용자 입장에서 1번의 Swap이 내부에서는 여러 번의 스왑의 결과일 수 있음. 따라서 최종적으로 나온 수량을 구하려면 마지막에 발생한 Swap Event를 봐야 함
- 해당 Event에서 amount out(받는 수량, to 수량)이 -(음수)로 나오기 때문에 isNegative로 값을 찾음
위와 같은 작업을 거치면 내가 원했던 V3 Swap에서 트랜잭션이 완료된 후 최종적으로 받게 되는 수량을 정확히 구할 수 있다.
3.~4. 내용은 필자가 사용한 Swap Event에 국한된 로직으로 디코딩되는 event의 특성에 따라 달라지는 부분임을 참고하시길 바란다.