feat: consistent precompile calling convention
- 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
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