메인 콘텐츠로 건너뛰기
이 페이지는 StableNet의 WEMIX 스타일 수수료 위임 메커니즘을 문서화합니다.
수수료 위임은 별도의 계정(FeePayer)이 트랜잭션 가스 비용을 지불하고, 트랜잭션 발신자(From)는 값 전송만 제공할 수 있도록 하는 구조입니다.
이 기능은 애플리케이션 운영자가 사용자 트랜잭션 비용을 보조하는 사용 사례를 가능하게 합니다.
가스 수수료 정책 및 팁 요구사항에 대한 정보는 가스 수수료 정책을 참조하세요.
일반 트랜잭션 처리 흐름은 트랜잭션 라이프사이클을 참조하세요.

목적 및 범위

수수료 위임은 트랜잭션의 재정적 책임을 두 당사자 간에 명확히 분리합니다.
  • 발신자(From)
    트랜잭션 의도를 서명하고 전송할 Value를 제공하며 유효한 nonce를 보유해야 합니다.
  • 수수료 지불자(FeePayer)
    가스 지불을 승인하기 위해 별도로 서명하며 가스 비용을 충당할 수 있는 충분한 잔액을 보유해야 합니다.
이 메커니즘은 전용 트랜잭션 타입 FeeDelegateDynamicFeeTxType = 0x16을 통해 구현되며 Applepie 포크가 활성화되어 있어야 합니다.

트랜잭션 타입 및 구조

트랜잭션 타입 정의

수수료 위임은 트랜잭션 타입 0x16(10진수 22)을 사용합니다.
ConstantValueDescription
FeeDelegateDynamicFeeTxType0x16Fee delegated EIP-1559 style transaction

핵심 데이터 구조

Message 구조체는 수수료 위임 필드를 포함합니다.
Message {
  From common.Address      // Transaction sender (signs transaction intent)
  To *common.Address      // Recipient
  Value *big.Int          // Value to transfer from sender
  GasLimit uint64
  GasPrice *big.Int
  GasFeeCap *big.Int
  GasTipCap *big.Int
  FeePayer *common.Address // Account paying for gas (optional)
  ...
}
TxData 인터페이스는 수수료 위임 관련 메서드를 포함합니다.
type TxData interface {
  feePayer() *common.Address
  rawFeePayerSignatureValues() (v, r, s *big.Int)
  ...
}

이중 서명 요구사항

수수료 위임 트랜잭션은 두 개의 독립적인 서명을 필요로 합니다.

서명 1: 트랜잭션 발신자

  • 트랜잭션 데이터(to, value, data, gas 매개변수, nonce)에 서명합니다.
  • 서명 값: V, R, S
  • 트랜잭션 실행 의도와 권한을 증명합니다.

서명 2: 수수료 지불자

  • 해당 트랜잭션에 대한 가스 지불에 동의함을 서명합니다.
  • 서명 값: FV, FR, FS
  • rawFeePayerSignatureValues() 메서드를 통해 조회됩니다.
이 이중 서명 구조는 발신자와 수수료 지불자 모두가 자신의 책임 범위에 대해 명시적으로 동의했음을 보장합니다.

상태 전환 프로세스

트랜잭션 메시지 변환

수수료 위임 트랜잭션은 상태 전환 진입 시 Message로 변환됩니다.
FeeDelegateDynamicFeeTx            Message
+--------------------------+       +---------------------------+
| SenderTx.Nonce           | ----> | Nonce                     |
| SenderTx.GasFeeCap       | ----> | GasFeeCap                 |
| SenderTx.GasTipCap       | ----> | GasTipCap                 |
| SenderTx.Gas             | ----> | GasLimit                  |
| SenderTx.To              | ----> | To                        |
| SenderTx.Value           | ----> | Value                     |
| SenderTx.Data            | ----> | Data                      |
| SenderTx.AccessList      | ----> | AccessList                |
| FeePayer                 | ----> | FeePayer                  |
+--------------------------+       +---------------------------+
                                   | From = recovered sender   |
                                   | GasPrice = effectivePrice |
                                   +---------------------------+
TransactionToMessage는 발신자 서명(V/R/S)으로부터 From 주소를 복구하고, tx.FeePayer()를 통해 FeePayer 필드를 설정합니다.
수수료 지불자의 서명은 가스 지불 승인에만 사용되며 From 주소 계산에는 사용되지 않습니다.

가스 구매(buyGas)

buyGas는 수수료 위임 시 분리된 잔액 검증과 차감을 수행합니다.

잔액 검증 규칙

ScenarioFeePayer Balance CheckSender Balance Check
Fee Delegation ActivegasLimit × gasPrice + blob feesvalue
Standard TransactionN/AgasLimit × gasPrice + value + blob fees

EVM 실행 컨텍스트

수수료 위임은 EVM 실행 의미론을 변경하지 않습니다.
  • tx.origin: 항상 트랜잭션 발신자(From)
  • 최상위 caller: 트랜잭션 발신자
  • FeePayer: EVM 컨텍스트에 노출되지 않음
이로 인해 스마트 컨트랙트는 수수료 위임 여부를 직접적으로 구분할 수 없습니다.

가스 환불 프로세스

트랜잭션 실행 후 미사용 가스는 실제 가스를 지불한 계정으로 환불됩니다.
func (st *StateTransition) refundGas(refundQuotient uint64) uint64 {
    refund := st.gasUsed() / refundQuotient
    if refund > st.state.GetRefund() {
        refund = st.state.GetRefund()
    }
    st.gasRemaining += refund
    remaining := uint256.NewInt(st.gasRemaining)
    remaining = remaining.Mul(remaining, uint256.MustFromBig(st.msg.GasPrice))
    if st.msg.FeePayer != nil {
        st.state.AddBalance(*st.msg.FeePayer, remaining)
    } else {
        st.state.AddBalance(st.msg.From, remaining)
    }
    st.gp.AddGas(st.gasRemaining)
    return refund
}

Coinbase에 대한 수수료 지불

검증자는 실제 가스 사용량에 기반한 수수료를 수령합니다.
수수료 지불은 상태 전환 후반부에서 수행되며, 수수료 지불자에 대한 블랙리스트 검증도 포함됩니다.

포크 활성화 요구사항

수수료 위임은 Applepie 포크 이후에만 허용됩니다.
if isFeeDelegation && !st.evm.ChainConfig().IsApplepie(st.evm.Context.BlockNumber) {
    return ErrTxTypeNotSupported
}
Applepie 이전 블록에서는 preCheck 단계에서 즉시 거부됩니다.

RPC를 통한 트랜잭션 구성

TransactionArgs 필드

FieldTypePurpose
From*common.AddressTransaction sender
FeePayer*common.AddressOptional fee payer
V,R,S*hexutil.BigSender signature
FV,FR,FSraw tx onlyFeePayer signature

JSON 표현

{
  "type": "0x16",
  "chainId": "0x2052",
  "nonce": "0x1",
  "to": "0xd46e8dd67c5d32be8058bb8eb970870f07244567",
  "gas": "0x76c0",
  "maxFeePerGas": "0x9184e72a000",
  "maxPriorityFeePerGas": "0x5f5e100",
  "value": "0xde0b6b3a7640000",
  "input": "0x",
  "accessList": [],
  "v": "0x0",
  "r": "0x...",
  "s": "0x...",
  "feePayer": "0x407d73d8a49eeb85d32cf465507dd71d507100c1",
  "fv": "0x0",
  "fr": "0x...",
  "fs": "0x..."
}

주요 불변량

  1. 잔액 격리
    FeePayer는 가스만, 발신자는 value만 부담합니다.
  2. Origin 보존
    tx.origin은 항상 발신자입니다.
  3. 이중 동의
    두 계정 모두 서명해야 트랜잭션이 유효합니다.
  4. 포크 게이트
    Applepie 활성화 이후에만 사용 가능합니다.
  5. 환불 일관성
    미사용 가스는 실제 지불자에게 반환됩니다.