Proposal: Support Solidity Custom Errors in Precompiles
- Dominant language
- Go
- Stars
- 164
- Forks
- 213
- Avg merge
- 3d 58m
- Merged PRs (30d)
- 12
Description
## 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
```sol
// 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
```go
// 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
}
```
2. Enhance ReturnRevertError
```go
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
```go
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.
```js
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
Contributor guide
Research direction
Start by reading the precompiles/common package, especially ReturnRevertError and the RunNativeAction call path, to understand the existing Error(string) encoding and interpreter return data handling. Add coverage for errors carrying custom ABI-encoded revert data while preserving the fallback behavior, and verify both paths return the expected data with vm.ErrExecutionReverted.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, solidity
- Domain
- backend-api-design, blockchain
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100