Titan의 트랜잭션 수수료로 TON을 사용하는 방법
L2 클라이언트 수정을 통한 fee token 구현
포스팅을 작성하는 데에 도움을 주신 suhyeon Lee, Steven Lee, Suah Kim님에게 감사를 표합니다.
You can check the English version of the article here.
Introduction
fee token( https://support.uniswap.org/hc/en-us/articles/18673568523789-What-is-a-token-fee-의 token fee와 동일한 의미로 사용되었습니다.)은 트랜잭션을 처리하는 데 사용되는 가스 비용을 지불하는 데에 활용되는 토큰을 의미합니다. 이러한 토큰은 주로 이더리움과 같은 스마트 계약을 실행하거나 자산을 전송하는데 필요한 가스 비용을 지불하는 데 사용됩니다.
Tokamak Network에서는 Titan에서 TON을 fee token으로 사용하기 위한 연구를 진행했습니다. fee token 기능이 지원되는 레이어2에서 사용자는 트랜잭션 요청 시 발생하는 거래 수수료를 ETH 대신 TON으로 지불할 수 있습니다.
이것이 왜 중요할까요? fee token으로 사용할 경우 TON에 대한 기본 수요를 보장하며 레이어2에서 지속적인 소각처로 사용됩니다. 사용자가 블록체인에서 어떤 애플리케이션이나 서비스를 사용하든 거래는 피할 수 없는 과정입니다. TON을 스테이킹하면 트랜잭션이 발생합니다. ETH를 TON으로 교환하는 것도 또한 트랜잭션입니다. 트랜잭션 수수료를 지불하기 위해 반드시 ETH를 Titan에 보유할 필요가 없습니다. 또한 Tokamak Network의 사용자들을 위한 혜택으로 일종의 할인율을 적용하여 ETH를 사용하는 것보다 훨씬 저렴한 가격으로 레이어2를 이용할 수 있도록 만들 수도 있습니다.
이는 Titan에서 TON의 지속적인 수요를 통한 가격 방어 효과를 제공할 뿐만아니라 ETH와 TON 잔액을 모두 유지하고 싶지 않은 사용자를 위한 편의성을 향상시킵니다. 또한 Tokamak Network 레이어 2에 액세스하고 다양한 Dapp을 이용하는 데 드는 비용을 줄이는 역할을 합니다.
본 아티클에서는 Titan의 geth 기반 L2 클라이언트를 직접 수정하여 트랜잭션 수수료를 ETH 대신 TON으로 지불하는 원리를 설명하고 예제 코드를 통해 fee token이 적용될 경우 Transfer와 Withdrawal에서 ETH와 TON 자산이 어떻게 변경되는지 확인해보겠습니다.
Customize L2 client for fee token
State Transition Model
fee token 기능을 구현하기 위해 우선 이더리움의 트랜잭션 수수료 계산 로직부터 살펴봅니다.
이더리움에서는 State Transition Model의 상태 전이 프로세스에서 트랜잭션 실행 시 소모되는 가스비를 계산하고 그에 따른 트랜잭션 수수료를 계산합니다. 트랜잭션을 요청하는 대상인 sender의 ETH 잔고를 확인, 계산된 트랜잭션 수수료를 차감하고 변경된 state를 확정합니다.
(트랜잭션의 발신자를 sender, 수신자를 receiver라고 할 때) 아래와 같이 수수료를 계산합니다.
- 최대 트랜잭션 수수료 (= Gas Limit * Gas Price)를 먼저 계산
- sender의 nonce를 체크하고 ETH 잔고에서 먼저 계산한 최대 트랜잭션 수수료를 차감 (이 때 sender의 ETH 잔고보다 최대 트랜잭션 수수료가 적으면 Error를 반환)
- receiver가 존재하지 않으면 evm.Create()를 호출하여 컨트랙트를 새로 생성
- evm.Call()을 호출하여 트랜잭션을 실행하고 결과값을 반환 (함수 실행 중 오류 발생 시 모든 State의 변경을 이전 스냅샷 상태로 복구)
- 이때, 코드를 수행한 만큼의 Gas (= Gas Used)는 Gas Price와 곱하여 실제 트랜잭션 수수료를 계산. 계산한 값은 miner의 잔고로 더해지고, 나머지 Gas는 sender에게 반환
TON 트랜잭션 수수료 계산
ETH 기준의 트랜잭션 수수료를 계산하고, 계산된 값을 TON으로 환산하여 TON 트랜잭션 수수료를 다음과 같이 계산할 수 있습니다.
Transaction Fee Based on TON = Transaction Fee Based on ETH * Price Ratio
ETH:TON 교환 비율인 Price Ratio를 얻기 위해 L2 네트워크에 TON_FeeVault 컨트랙트를 배포합니다. L2 클라이언트는 TON_FeeVault 컨트랙트의 storage에 저장된 Price Ratio를 사용합니다. TON_FeeVault 컨트랙트를 구성하는 주요 함수는 아래와 같습니다.
- updatePriceRatio(): Price Ratio를 업데이트합니다. 이 함수는 컨트랙트의 owner만 호출할 수 있습니다.
- updateGasPriceOracleAddress(): L2의 OVM_GasPriceOracle 컨트랙트의 주소를 업데이트합니다. 이 함수는 컨트랙트의 owner만 호출할 수 있습니다.
- withdrawTON(): L2를 운영하는 Sequencer의 트랜잭션 수수료 출금용 함수입니다. L2 네트워크에서 발생하는 트랜잭션 수수료 (TON)는 TON_FeeVault 컨트랙트로 전송됩니다. 이렇게 모인 트랜잭션 수수료를 Sequencer가 지정한 L1 Fee Wallet으로 출금합니다.
컨트랙트 코드는 https://github.com/tokamak-network/tokamak-titan/blob/feature/native-token/packages/tokamak/contracts/contracts/L2/fee-token/TON_FeeVault.sol 에서 확인할 수 있습니다.
Titan 트랜잭션 수수료 계산 방법
다음으로 Titan에서의 트랜잭션 수수료 계산 방법을 알아보겠습니다.
Optitmitic Rollup 기반의 L2인 Titan에서 발생하는 트랜잭션 수수료는 롤업 비용인 L1 Security Fee와 트랜잭션 실행 비용인 L2 Execution Fee의 합으로 나타낼 수 있습니다. L1 Security Fee는 L2 Execution Fee는 각각 아래와 같이 계산됩니다. (각 용어에 대한 설명과 자세한 내용은 https://tokamaknetwork.gitbook.io/home/v/kor/02-service-guide/titan/user-guide/l2-fee을 참고해주세요.)
L1 Security Fee = L1 Base Fee * (Gas of Calldata + Overhead) * Scalar
L2 Execution Fee = L2 Gas Price * Gas Used
L2에 배포된 OVM_GasPriceOracle 컨트랙트를 통해 L1 Base Fee, Overhead, Scalar, L2 Gas Price를 조회할 수 있습니다. Gas of Calldata와 Gas Used는 L2 클라이언트에서 직접 계산합니다.
L2 클라이언트에 fee token 구현
이제 L2 클라이언트에서 fee token 구현을 위해 수정한 코드 로직을 살펴보겠습니다.
아래 NewStateTranstion 함수는 State Transition Object를 초기화합니다. evm.ChainConfig().IsFeeTokenUpdate(evm.BlockNumber)는 아규먼트로 주어진 블록 번호가 fee token 업그레이드가 적용된 시점인지 확인하는 함수입니다. 만약 fee token 업그레이드가 적용되어 L2에서 TON을 트랜잭션 수수료로 사용한다면 true를 반환합니다.
isFeeTokenUpdate가 true일 경우 evm.StateDB.GetTonPriceRatio()를 호출하여 Price Ratio를 조회합니다. L1으로 rollup되는 L2 트랜잭션일 경우에는 L1 Security Fee에 Price Ratio를 곱한 값을 사용합니다. L2 Gas Price에도 Price Ratio를 곱하여 L2 Gas Price를 TON 단위로 전환합니다.
반환값인 StateTransition 구조체는 https://github.com/tokamak-network/tokamak-titan/blob/feature/native-token/l2geth/core/state_transition.go#L58 에 정의되어 있으며 TransitionDb()에서 트랜잭션 수수료를 계산할 때 사용됩니다.
TransitionDb() 내 주요 함수를 살펴보겠습니다. TransitionDb()은 트랜잭션 실행 결과와 Gas Used (트랜잭션 실행에 사용한 가스의 양)를 반환합니다.
- preCheck(): sender의 nonce를 확인하고 최대 트랜잭션 수수료를 계산하여 미리 sender의 TON 잔고에서 차감
- Create/Call(): receiver에 따라 컨트랙트 생성 혹은 트랜잭션을 실행하고 그 결과를 반환
- refundGas(): 트랜잭션 실행 후 남은 gas 기반으로 환불할 수수료를 계산하여 sender에게 반환
이어서 트랜잭션 수수료를 계산합니다. st.gasUsed()와 st.gasPrice를 곱하여 l2Fee를 계산하고 st.l1Fee와 l2Fee를 합하여 트랜잭션 수수료를 구합니다.
st.isFeeTokenUpdate에 따라 true일 경우 트랜잭션 수수료만큼 TON_FeeVault의 TON 잔고를 증가시킵니다.
결과적으로 TransitionDb() 함수가 실행되면 L2 트랜잭션을 실행하여 소비되는 가스만큼 트랜잭션 수수료를 계산합니다. 계산한 액수만큼 sender의 TON 잔고가 차감되고 TON_FeeVault 컨트랙트의 TON 잔고가 증가합니다.
Demonstration
테스트 환경
fee token을 테스트하기 위해 Titan과 동일한 사양의 네트워크를 로컬 환경에 구축합니다.
- repository: https://github.com/tokamak-network/tokamak-titan/tree/feature/native-token
- commit: 573c930
테스트용 네트워크 실행
fee token 예제를 테스트하기 위해 아래와 같이 테스트용 네트워크를 실행합니다.
git clone -b feature/native-token - single-branch https://github.com/tokamak-network/tokamak-titan.git
cd tokamak-titan
# build
docker-compose -f ./ops/docker-compose-fee-token.yml build
# up
docker-compose -f ./ops/docker-compose-fee-token.yml up -d
# down
docker-compose -f ./ops/docker-compose-fee-token.yml down
예제 실행
총 3개의 예제로 fee token 기능을 확인해보겠습니다.
- ETH 전송
예제 코드에서는 0.001 ETH를 ether.js의 sendTransaction 함수를 이용하여 전송하고 sender의 ETH와 TON 잔고 변화, 계산된 트랜잭션 수수료를 출력합니다. 예제 코드 실행 시 아래와 같은 결과를 확인할 수 있습니다. sender의 잔고에서 전송한 ETH 수량만 차감되었고 트랜잭션 수수료로 인해 TON이 차감되었음을 알 수 있습니다.
...
Transaction hash: 0x2e16d471c56564b631fcb0b9ab6106628a1ff3a7d1ff8a1b9c504675accfbcfa
Change in ETH balance: 0.001
change in TON balance: 0.3537894186506904
L1Fee: 0.3537891592754904
L2Fee: 0.0000002593752
Total Fee: 0.3537894186506904
- TON 전송
1 TON을 ERC20.transfer 함수를 이용하여 전송하고 sender의 ETH와 TON 잔고 변화, 계산된 트랜잭션 수수료를 출력합니다. 예제 코드 실행 시 아래와 같은 결과를 확인할 수 있습니다. sender의 ETH 잔고는 변동이 없고 TON 잔고는 전송한 1 TON과 트랜잭션 수수료가 차감되었습니다.
...
Transaction hash: 0xca355143a677a194476da1d56250ae5a6c6d46bf49496c0a6f13906910c00b43
Change in ETH balance: 0.0
change in TON balance: 1.3987911467022376
L1Fee: 0.3987907573430088
L2Fee: 0.0000003893592288
Total Fee: 0.3987911467022376
- ETH 출금
이제 ETH를 L2에서 L1으로 출금해보겠습니다. L2StandardBridge 컨트랙트의 withdraw 함수를 이용하여 0.01 ETH를 출금합니다. 테스트 네트워크에서 실행 중인 Batch Submitter는 L2에서 발생한 트랜잭션을 감지하여 트랜잭션 batch와 state root를 L1에 롤업합니다. Message Relayer에 의해 L2 트랜잭션state의 롤업 여부를 확인하고 L1에서 릴레이 트랜잭션이 실행되면 출금이 완료됩니다. 출금이 완료되면 출금 전/후의 ETH 잔고와 TON 잔고를 확인할 수 있습니다. ETH 출금임에도 TON의 잔고가 트랜잭션 수수료에 의해 차감되었음을 볼 수 있습니다.
---------------- Widthdraw 0.001 ETH ----------------
TX Hash: 0x8fcf24031721747f0fbd519a15b4f39dcac584000f1d1595e2c94b883402d2ed
Before L1: 0.198185314318598221 ETH L2: 0.061779738056659911 ETH
Before L2: 5.3517209343875952 TON
.............
After 14 seconds
After L1: 0.199185314318598221 ETH L2: 0.060779738056659911 ETH
After L2: 4.9270767220854416 TON
전체 예제 코드 및 실행 가이드는 Tokamak Network의 Repository https://github.com/tokamak-network/tokamak-titan-example 에서 확인할 수 있습니다.
- ETH/TON 전송 예제: https://github.com/tokamak-network/tokamak-titan-example/tree/main/fee-token-l2-transfer
- ETH 출금 예제: https://github.com/tokamak-network/tokamak-titan-example/tree/main/fee-token-withdraw-eth
Next Challenges
지금까지 Titan에서 fee token 기능을 구현하고 L2 트랜잭션에 따른 sender의 TON과 ETH 잔고 변화까지 예제를 통해 확인했습니다. 하지만 아직은 우리가 풀어야 할 몇 가지 문제가 있습니다.
Price Ratio의 참조 및 조정
Price Ratio를 업데이트할 때 그 근거가 되는 데이터가 신뢰성을 가져야 하고 일관된 규칙에 의해 관리되어야 합니다. TON_FeeVault 컨트랙트에 의해 관리되는 Price Ratio는 사용자에게 부과되는 트랜잭션 비용과 밀접한 관계를 가집니다. 현재 구조에서 Price Ratio는 오직 TON_FeeVault의 owner만 조정이 가능하고 L2를 운영하는 Sequencer가 주로 이 역할을 담당하게 됩니다. 이는 Sequencer에 의해 트랜잭션 수수료가 얼마든지 바뀔 수 있는 중앙화된 구조입니다.
메타마스크 사용 문제
fee token을 도입하면 트랜잭션 수수료로 TON을 사용하기 때문에 sender의 ETH 잔고가 0이어도 트랜잭션을 요청하여 정상적으로 결과를 확인할 수 있습니다. L2 클라이언트에 직접 트랜잭션을 요청할 때는 문제가 없지만 메타마스크를 사용할 경우 아래 그림과 같이 sender의 ETH 잔고가 0일 때 사용자가 예상 트랜잭션 수수료를 확인하는 단계에서 예상 수수료보다 ETH 잔고가 적기 때문에 ‘insuffient blalance’ 에러가 발생합니다. 트랜잭션 승인 단계에서는 전송 기능이 막혀있는 것을 확인할 수 있습니다.
이러한 문제들을 해결하고 Tokamak Network 레이어2 네트워크에서 ETH의 역할을 TON으로 완전히 대체하기 위해서 TON을 기본 통화로 사용하는 새 레이어2 네트워크를 제공할 예정입니다. 이는 RPC 문제 뿐만 아니라 EVM 기반의 스마트 컨트랙트를 그대로 Tokamak Network의 레이어2 네트워크에 배포하여 사용할 수 있습니다. 또한 새 레이어2 네트워크에서는 사용자에게 합리적인 수수료를 제공하기 위해 Price Ratio를 시장 가격의 변화에 따라 동적으로 업데이트할 수 있는 오라클 서비스를 도입할 예정입니다. 시장 가격에 따라 Price Ratio가 자동으로 업데이트되므로 Sequencer는 수동으로 업데이트 트랜잭션을 요청할 필요가 없으며 모두에게 공개된 시장 가격을 기반으로 Price Ratio를 조정하기 때문에 사용자가 믿고 수수료를 지불할 수 있습니다.
Conclusion
본 아티클에서는 TON을 트랜잭션 수수료로 사용하는 fee token 구현 방법과 함께 test network에서 예제 코드를 통한 실제 ETH와 TON의 자산 변화를 확인했습니다. 더 자세한 fee token 구현 코드를 확인하고 싶으신 분들은https://github.com/tokamak-network/tokamak-titan/tree/feature/native-token 을 참고해주세요.
Tokamak Network에서는 앞서 언급한 문제들을 해결하고 TON을 트랜잭션 수수료 뿐만 아니라 레이어2에서의 기본 통화로 사용하는 ‘native TON’의 개념을 새로운 Titan에 적용하기 위한 연구를 진행 중입니다. 내년 출시될 새 레이어2 네트워크는 Optimism Bedrock 기반의 코어 프로토콜과 함께 ETH의 역할을 완전히 대체하는 native TON을 지원할 예정입니다. 새 레이어2 네트워크의 오픈을 준비하는 동안 저희의 연구 결과와 진행 내용을 미디움에 실시간으로 포스팅할 예정이니 많은 관심 부탁드립니다.