cosmos / cosmos/evm

feat: consistent precompile calling convention

Open
#605 2 comments 2 reactions 0 assignees View on GitHub
Dominant language
Go
Stars
164
Forks
213
Avg merge
3d 58m
Merged PRs (30d)
12

Description

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

* Change `runPrecompiledContract` to build the `Contract` instance in the same way as EVM itself, for example `NewContract(originCaller, caller)` in the case of delegatecall, so the precompiles can distinguish different types of calls.
https://github.com/cosmos/go-ethereum/pull/10

Contributor guide

Open the contributing guide

Research direction

Start with the precompile Run method and runPrecompiledContract, then compare how the EVM constructs Contract instances for call, delegatecall, and callcode. Review NewContract and the referenced cosmos/go-ethereum PR. Done means precompiles can distinguish the call context, enforce read-only execution, and no longer need a separate readOnly Run parameter.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
blockchain
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.