메인 콘텐츠로 건너뛰기
x/evm 모듈은 Tx 처리 로직을 외부에서 확장하고 사용자 정의할 수 있는 EvmHooks 인터페이스를 구현합니다. 이는 EVM 컨트랙트가 다음을 통해 네이티브 cosmos 모듈을 호출할 수 있도록 지원합니다:
  1. log signature를 정의하고 스마트 컨트랙트에서 특정 로그를 내보내고,
  2. 네이티브 트랜잭션 처리 코드에서 해당 로그를 인식하고,
  3. 네이티브 모듈 호출로 변환합니다.
이를 위해 인터페이스에는 EvmKeeper에 사용자 정의 Tx hook을 등록하는 PostTxProcessing hook이 포함되어 있습니다. 이러한 Tx hook은 EVM 상태 전환이 완료되고 실패하지 않은 후에 처리됩니다. EVM 모듈에는 기본 hook이 구현되어 있지 않습니다.
type EvmHooks interface {
	// Must be called after tx is processed successfully, if return an error, the whole transaction is reverted.
	PostTxProcessing(ctx sdk.Context, msg core.Message, receipt *ethtypes.Receipt) error
}

PostTxProcessing

PostTxProcessing은 EVM 트랜잭션이 성공적으로 완료된 후에만 호출되며 기본 hook에 호출을 위임합니다. 등록된 hook이 없으면 이 함수는 nil 오류와 함께 반환됩니다.
func (k *Keeper) PostTxProcessing(ctx sdk.Context, msg core.Message, receipt *ethtypes.Receipt) error {
	if k.hooks == nil {
		return nil
	}
	return k.hooks.PostTxProcessing(k.Ctx(), msg, receipt)
}
EVM 트랜잭션과 동일한 cache context에서 실행되며, 오류를 반환하면 전체 EVM 트랜잭션이 되돌려집니다. hook 구현자가 트랜잭션을 되돌리고 싶지 않으면 항상 nil을 반환할 수 있습니다. hook이 반환하는 오류는 VM 오류 failed to process native logs로 변환되고 자세한 오류 메시지는 반환 값에 저장됩니다. 메시지는 네이티브 모듈로 비동기적으로 전송되며 호출자가 오류를 catch하고 복구할 방법이 없습니다.

Use Case: Call Native ERC20 Module on Injective

다음은 EVMHooks가 ERC-20 Token을 Cosmos 네이티브 Coin으로 변환하기 위해 네이티브 모듈을 호출하는 컨트랙트를 지원하는 방법을 보여주는 Injective erc20 모듈에서 가져온 예입니다. 위의 단계를 따릅니다. 다음과 같이 스마트 컨트랙트에서 Transfer log signature를 정의하고 내보낼 수 있습니다:
event Transfer(address indexed from, address indexed to, uint256 value);

function _transfer(address sender, address recipient, uint256 amount) internal virtual {
  require(sender != address(0), "ERC20: transfer from the zero address");
  require(recipient != address(0), "ERC20: transfer to the zero address");

  _beforeTokenTransfer(sender, recipient, amount);

  _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance");
  _balances[recipient] = _balances[recipient].add(amount);
  emit Transfer(sender, recipient, amount);
}
애플리케이션은 EvmKeeperBankSendHook을 등록합니다. ethereum 트랜잭션 Log를 인식하고 bank 모듈의 SendCoinsFromAccountToAccount 메서드 호출로 변환합니다:

const ERC20EventTransfer = "Transfer"

// PostTxProcessing implements EvmHooks.PostTxProcessing
func (k Keeper) PostTxProcessing(
	ctx sdk.Context,
	msg core.Message,
	receipt *ethtypes.Receipt,
) error {
	params := h.k.GetParams(ctx)
	if !params.EnableErc20 || !params.EnableEVMHook {
		// no error is returned to allow for other post processing txs
		// to pass
		return nil
	}

	erc20 := contracts.ERC20BurnableContract.ABI

	for i, log := range receipt.Logs {
		if len(log.Topics) < 3 {
			continue
		}

		eventID := log.Topics[0] // event ID

		event, err := erc20.EventByID(eventID)
		if err != nil {
			// invalid event for ERC20
			continue
		}

		if event.Name != types.ERC20EventTransfer {
			h.k.Logger(ctx).Info("emitted event", "name", event.Name, "signature", event.Sig)
			continue
		}

		transferEvent, err := erc20.Unpack(event.Name, log.Data)
		if err != nil {
			h.k.Logger(ctx).Error("failed to unpack transfer event", "error", err.Error())
			continue
		}

		if len(transferEvent) == 0 {
			continue
		}

		tokens, ok := transferEvent[0].(*big.Int)
		// safety check and ignore if amount not positive
		if !ok || tokens == nil || tokens.Sign() != 1 {
			continue
		}

		// check that the contract is a registered token pair
		contractAddr := log.Address

		id := h.k.GetERC20Map(ctx, contractAddr)

		if len(id) == 0 {
			// no token is registered for the caller contract
			continue
		}

		pair, found := h.k.GetTokenPair(ctx, id)
		if !found {
			continue
		}

		// check that conversion for the pair is enabled
		if !pair.Enabled {
			// continue to allow transfers for the ERC20 in case the token pair is disabled
			h.k.Logger(ctx).Debug(
				"ERC20 token -> Cosmos coin conversion is disabled for pair",
				"coin", pair.Denom, "contract", pair.Erc20Address,
			)
			continue
		}

		// ignore as the burning always transfers to the zero address
		to := common.BytesToAddress(log.Topics[2].Bytes())
		if !bytes.Equal(to.Bytes(), types.ModuleAddress.Bytes()) {
			continue
		}

		// check that the event is Burn from the ERC20Burnable interface
		// NOTE: assume that if they are burning the token that has been registered as a pair, they want to mint a Cosmos coin

		// create the corresponding sdk.Coin that is paired with ERC20
		coins := sdk.Coins{{Denom: pair.Denom, Amount: sdk.NewIntFromBigInt(tokens)}}

		// Mint the coin only if ERC20 is external
		switch pair.ContractOwner {
		case types.OWNER_MODULE:
			_, err = h.k.CallEVM(ctx, erc20, types.ModuleAddress, contractAddr, true, "burn", tokens)
		case types.OWNER_EXTERNAL:
			err = h.k.bankKeeper.MintCoins(ctx, types.ModuleName, coins)
		default:
			err = types.ErrUndefinedOwner
		}

		if err != nil {
			h.k.Logger(ctx).Debug(
				"failed to process EVM hook for ER20 -> coin conversion",
				"coin", pair.Denom, "contract", pair.Erc20Address, "error", err.Error(),
			)
			continue
		}

		// Only need last 20 bytes from log.topics
		from := common.BytesToAddress(log.Topics[1].Bytes())
		recipient := sdk.AccAddress(from.Bytes())

		// transfer the tokens from ModuleAccount to sender address
		if err := h.k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, recipient, coins); err != nil {
			h.k.Logger(ctx).Debug(
				"failed to process EVM hook for ER20 -> coin conversion",
				"tx-hash", receipt.TxHash.Hex(), "log-idx", i,
				"coin", pair.Denom, "contract", pair.Erc20Address, "error", err.Error(),
			)
			continue
		}
	}

	return nil
마지막으로 app.go에서 hook을 등록합니다:
app.EvmKeeper = app.EvmKeeper.SetHooks(app.Erc20Keeper)