Meta transaction

Meta transaction (ERC-2771) 에 대해서 알아보자.

메타 트랜잭션(meta transaction)이란 개념이있다.

일반적으로, 블록체인을 처음 접하는 사용자가 DApp 을 이용한다면 사용하기 매우 어려울 것이다.

메타마스크/팬텀 등으로 지갑을 생성하고, 거래소나 기타 루트를 통해 가상화폐를 본인 지갑에 입금하는 등 알아야 할 개념들이 매우 많다.

메타 트랜잭션은 사용자의 가스비를 대납하는 트랜잭션이다.

위와 같은 상황에서, 사용자는 가상화폐 입금절차 없이 지갑 생성, 연결만 하면 트랜잭션을 발생시킬수 있다.

다들 알겠지만 당연히 가스는 지불해야 한다.

단, 그것을 트랜잭션을 요청하는 실 사용자가 수행하지 않고 ‘대납자’가 수행한다.

사용자는 일반적인 트랜잭션이 생성되는 방식과 유사한 방식으로 트랜잭션을 만들 수 있으며, 자체 개인키로 서명하지만 체인에 직접 보내는 대신 대납자(relayer, sender, payer, … etc)에게 트랜잭션을 전송한다.

이에 대한 표준으로 많이 알려진 것이 ERC-2771이다.

오픈제플린에서는 ERC2771Forwarder, ERC2771Context 컨트랙트를 사용해서 메타트랜잭션을 구현해두었다.

Forwarder 는 트랜잭션을 Relay 하는 역할을 담당하며, ERC2771Context 는 메타트랜잭션을 통해 실제로 호출할 컨트랙트에서 상속받으면 된다.

오픈제플린에서 제시하는 컨트랙트를 살펴보자.

function execute(ForwardRequestData calldata request) public payable virtual {
        // We make sure that msg.value and request.value match exactly.
        // If the request is invalid or the call reverts, this whole function
        // will revert, ensuring value isn't stuck.
        if (msg.value != request.value) {
            revert ERC2771ForwarderMismatchedValue(request.value, msg.value);
        }

        if (!_execute(request, true)) {
            revert Errors.FailedCall();
        }
    }

메타 트랜잭션을 구현하고자 한다면 위의 함수를 호출하면 된다. ForwardRequestData 는 다음과 같이 생겼다.

struct ForwardRequestData {
        address from;
        address to;
        uint256 value;
        uint256 gas;
        uint48 deadline;
        bytes data;
        bytes signature;
}
  • 유저의 주소

  • 호출할 컨트랙트

  • 트랜잭션의 value 로 담을 ETH(or else)

  • 소비할 Gas

  • 트랜잭션의 유효기간

  • 호출할 컨트랙트의 메소드 ABI Encoded data

  • 위 트랜잭션을 서명하고 받은 signature

execute 함수는 _execute 를 실행해서 호출할 컨트랙트로 트랜잭션을 Relay 한다.

function _execute(
        ForwardRequestData calldata request,
        bool requireValidRequest
    ) internal virtual returns (bool success) {
        (bool isTrustedForwarder, bool active, bool signerMatch, address signer) = _validate(request);

        // Need to explicitly specify if a revert is required since non-reverting is default for
        // batches and reversion is opt-in since it could be useful in some scenarios
        if (requireValidRequest) {
            if (!isTrustedForwarder) {
                revert ERC2771UntrustfulTarget(request.to, address(this));
            }

            if (!active) {
                revert ERC2771ForwarderExpiredRequest(request.deadline);
            }

            if (!signerMatch) {
                revert ERC2771ForwarderInvalidSigner(signer, request.from);
            }
        }

        // Ignore an invalid request because requireValidRequest = false
        if (isTrustedForwarder && signerMatch && active) {
            // Nonce should be used before the call to prevent reusing by reentrancy
            uint256 currentNonce = _useNonce(signer);

            uint256 reqGas = request.gas;
            address to = request.to;
            uint256 value = request.value;
            bytes memory data = abi.encodePacked(request.data, request.from);

            uint256 gasLeft;

            assembly {
                success := call(reqGas, to, value, add(data, 0x20), mload(data), 0, 0)
                gasLeft := gas()
            }

            _checkForwardedGas(gasLeft, request);

            emit ExecutedForwardRequest(signer, currentNonce, success);
        }
}

_execute 함수에서는 서명을 검증하고, 호출할 컨트랙트가 trustedForwarder 인지 확인한다.

그리고 assembly 를 이용해 호출할 컨트랙트(to)를 호출하는데, to 컨트랙트에서는 아래의 ERC2771Context 메소드를 이용해 msg.sender 를 가져올 수 있다.

function _msgSender() internal view virtual override returns (address) {
        uint256 calldataLength = msg.data.length;
        uint256 contextSuffixLength = _contextSuffixLength();
        if (isTrustedForwarder(msg.sender) && calldataLength >= contextSuffixLength) {
            return address(bytes20(msg.data[calldataLength - contextSuffixLength:]));
        } else {
            return super._msgSender();
        }
}

msg.data의 length 를 가져와서 20(_contextSuffixLength)보다 큰 경우, msg.sender 가 아닌 data 에 있는 주소를 가져온다.

즉, 대납자가 아닌 사용자의 주소를 가져온다.

과연 최선일까?

Alchemy 에서는 계정 추상화 (ERC-4337) 를 추천한다.

계정 추상화에는 UserOps, Paymasters 라는 개념이 있다.

아래는 Alchemy 에서 발췌한 글.

UserOps

ERC-4337 계정 추상화 표준의 가스 추상화 부분에는 Paymasters가 도입되었다 .

페이마스터는 유효한 가스 후원을 정의하는 데 사용할 수 있는 임의의 검증 논리를 갖춘 온체인 스마트 계약 이다.

여기서도 차이점은 실행이 onchain 이라는 것이다 .

DAO, dapp 및 기타 팀은 ERC-20 가스 결제 등과 같은 기능을 갖춘 맞춤형 페이마스터를 배포할 수 있다.

이러한 맞춤형 Paymaster는 ERC-4337을 사용하여 기존 Bundler 서비스와 연결하여 사용할 수 있다.

이는 공급자 채택이 필요한 메타 트랜잭션과 다르다.

Realyer vs Paymaster

Meta Transaction 에서의 Relayer는 인프라 공급자가 관리하는 개인 키인 반면,

Account Abstraction Bundler는 표준화된 노드다.

서로 다른 번들러 간 전환은 API 키와 API URL을 변경하는 것만큼 간단하다.

후원 검증이 Paymaster 계약 내에서 온체인으로 수행되므로 계정 추상화에는 MinimalForwarder 개념이 없다.

메타 트랜잭션의 경우 기본 트랜잭션 내에 하나의 트랜잭션만 포함하는 대신 번들러는 여러 UserOperations를 하나의 번들(하나의 기본 트랜잭션)로 묶는다!

Last updated