Skip to content

Proposal: Support Solidity Custom Errors in Precompiles #954

@nowooj

Description

@nowooj

Background

Currently, the precompiles/common package in cosmos/evm always encodes revert data as Error(string) format when precompile execution fails (see ReturnRevertError function).

However, Custom Errors introduced in Solidity 0.8.4+ offer several advantages:

  • Gas efficient (only selector is stored, no message string needed)
  • Type safe (error argument types are checked)
  • Distinguishable by error selector on the client side

For example

//  Error.sol
error InvalidAddress(address addr);
error InvalidAmount(uint256 amount);

Using these custom errors allows transaction receipts to distinguish error types by selector, making debugging and error handling much easier.

Problem

Currently, RunNativeAction always encodes errors as Error(string) format by calling ReturnRevertError(evm, err):

Proposal

Add support for custom revert data in the precompiles/common package:

  1. Add RevertDataCarrier Interface
// RevertDataCarrier is an error that carries custom ABI-encoded revert data
// (error selector + arguments) instead of a string message.
type RevertDataCarrier interface {
    error
    RevertData() []byte
}
  1. Enhance ReturnRevertError
func ReturnRevertError(evm *vm.EVM, err error) ([]byte, error) {
    // Use custom revert data if available
    if carrier, ok := err.(RevertDataCarrier); ok {
        data := carrier.RevertData()
        evm.Interpreter().SetReturnData(data)
        return data, vm.ErrExecutionReverted
    }
    
    // Fallback to existing behavior: Error(string) format
    revertReasonBz, encErr := evmtypes.RevertReasonBytes(err.Error())
    if encErr != nil {
        return nil, vm.ErrExecutionReverted
    }
    evm.Interpreter().SetReturnData(revertReasonBz)
    return revertReasonBz, vm.ErrExecutionReverted
}

Usage Example

recipientCosmosAddr, err := p.accountKeeper.AddressCodec().BytesToString(recipientEvmAddr.Bytes())
if err != nil {
    // Get error definition from ABI
    errorABI := abi.Errors["InvalidAddress"]
    
    // Pack arguments
    data, _ := errorABI.Inputs.Pack(recipientEvmAddr)
    
    // selector (4 bytes) + data
    revertData := append(errorABI.ID.Bytes()[:4], data...)
    
    // Return custom revert data
    return nil, &RevertWithData{Data: revertData}
}

Benefits

Errors defined in the contract can be easily handled by the client or contract.

const staking = await hre.ethers.getContractAt('StakingI', STAKING_PRECOMPILE_ADDRESS);

try {
      await staking.connect(signer).delegate.staticCall(delegatorAddress, validatorAddress, amount, { gasLimit: GAS_LIMIT });
  } catch (e) {
      const revertData = e.data

      // Decode revert data via contract interface (selector + args)
      const decoded = staking.interface.parseError(revertData);
      expect(decoded, 'revert data should decode to a known error').to.exist;
      expect(decoded.name).to.equal('InvalidAddress', 'revert should be InvalidAddress');

      //  InvalidAddress(address addr)
      const [address] = decoded.args;
      expect(address.toLowerCase()).to.equal(signer.address.toLowerCase(), 'invalid address');
  }

Note

I've implemented this feature locally in our project by creating a precompiles/common package, but official support in cosmos/evm would be beneficial for the ecosystem. If this direction is good, I think I can easily contribute

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions