Skip to content

feat: consistent precompile calling convention #605

@yihuang

Description

@yihuang

Recall the 4 types of calls in EVM: call/delegatecall/callcode/staticcall:

  • call: normal message call

    addr1 {
      addr2.call()
    }
    

    the context of the addr2 contract execution:

    contract.caller: addr1
    contract.address: addr2
    interpreter.readOnly: false
    
  • staticcall: read-only message call

    addr1 {
      addr2.staticcall()
    }
    

    the context of the addr2 contract execution:

    contract.caller: addr1
    contract.address: addr2
    interpreter.readOnly: true
    
  • callcode: message call into itself with another account's code

    addr1 {
      addr2.callcode()
    }
    

    the context of the addr2 contract execution:

    contract.caller: addr1
    contract.address: addr1
    interpreter.readOnly: false
    
  • delegatecall: like callcode, but keeping the current msg.sender and msg.value.

    addr1 {
      addr2.delegatecall()
    }
    

    the context of the addr2 contract execution:

    contract.caller: originCaller
    contract.address: addr1
    interpreter.readOnly: false
    

Cosmos Precompiles

Let's say the addr1 is an user contract which calls a precompile at the address addr2.

For the cosmos precompiles that will access the cosmos storages:

  1. They should never be executed on the storage of another contract, that means contract.address == addr2 should always be true, that'll rule out both delegatecall and callcode.

  2. They should respect the readOnly semantic of staticcall, for normal contracts they'll check the interpreter.readOnly flag:

    if interpreter.readOnly {
      return nil, ErrWriteProtection
    }
    

    But for precompiles, we check the readOnly parameter passed to the Run method.

Proposal

The contract.address can be read from existing parameters in default Run method of precompile (but we need to change runPrecompiledContract to build the contract instance in the same way as EVM itself):

func (p Precompile) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) (bz []byte, err error) {
  if contract.Address() != p.Address() {
    return nil, errors.New("deletatecall and callcode are not supported")
  }
}

func (p Precompile) WritableMethod(evm *vm.EVM) (bz []byte, err error) {
  if evm.Interpreter().ReadOnly() {
    return nil, vm.ErrWriteProtection
  }
}

As a result, we can remove the readOnly parameter from the Run method, one less thing to patch the go-ethereum.

Patch go-ethereum

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