bug in the respective layer 0/1/2 network code that results in unintended smart contract behavior with no concrete funds at direct risk
(Specifications) A bug in specifications with no direct impact on client implementations
Description
Brief/Intro
Within the contract deployment in the Erigon, it misses the check that the newly created contract address does not belong to the precompile contract addresses, which leads to the address collision.
func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gasRemaining uint64, value *uint256.Int, address libcommon.Address, typ OpCode, incrementNonce bool, bailout bool) ([]byte, libcommon.Address, uint64, error) {
var ret []byte
var err error
var gasConsumption uint64
depth := evm.interpreter.Depth()
if evm.config.Debug {
if depth == 0 {
evm.config.Tracer.CaptureStart(evm, caller.Address(), address, false /* precompile */, true /* create */, codeAndHash.code, gasRemaining, value, nil)
defer func() {
evm.config.Tracer.CaptureEnd(ret, gasConsumption, err)
}()
} else {
evm.config.Tracer.CaptureEnter(typ, caller.Address(), address, false /* precompile */, true /* create */, codeAndHash.code, gasRemaining, value, nil)
defer func() {
evm.config.Tracer.CaptureExit(ret, gasConsumption, err)
}()
}
}
// Depth check execution. Fail if we're trying to execute above the
// limit.
if depth > int(params.CallCreateDepth) {
err = ErrDepth
return nil, libcommon.Address{}, gasRemaining, err
}
if !evm.Context.CanTransfer(evm.intraBlockState, caller.Address(), value) {
if !bailout {
err = ErrInsufficientBalance
return nil, libcommon.Address{}, gasRemaining, err
}
}
if incrementNonce {
nonce := evm.intraBlockState.GetNonce(caller.Address())
if nonce+1 < nonce {
err = ErrNonceUintOverflow
return nil, libcommon.Address{}, gasRemaining, err
}
evm.intraBlockState.SetNonce(caller.Address(), nonce+1)
}
// We add this to the access list _before_ taking a snapshot. Even if the creation fails,
// the access-list change should not be rolled back
if evm.chainRules.IsBerlin {
evm.intraBlockState.AddAddressToAccessList(address)
}
// Ensure there's no existing contract already at the designated address
contractHash := evm.intraBlockState.ResolveCodeHash(address)
if evm.intraBlockState.GetNonce(address) != 0 || (contractHash != (libcommon.Hash{}) && contractHash != emptyCodeHash) {
err = ErrContractAddressCollision
return nil, libcommon.Address{}, 0, err
}
// Create a new account on the state
snapshot := evm.intraBlockState.Snapshot()
evm.intraBlockState.CreateAccount(address, true)
if evm.chainRules.IsSpuriousDragon {
evm.intraBlockState.SetNonce(address, 1)
}
evm.Context.Transfer(evm.intraBlockState, caller.Address(), address, value, bailout)
// Initialise a new contract and set the code that is to be used by the EVM.
// The contract is a scoped environment for this execution context only.
contract := NewContract(caller, address, value, gasRemaining, evm.config.SkipAnalysis)
contract.SetCodeOptionalHash(&address, codeAndHash)
if evm.config.NoRecursion && depth > 0 {
return nil, address, gasRemaining, nil
}
ret, err = run(evm, contract, nil, false)
// EIP-170: Contract code size limit
if err == nil && evm.chainRules.IsSpuriousDragon && len(ret) > evm.maxCodeSize() {
// Gnosis Chain prior to Shanghai didn't have EIP-170 enabled,
// but EIP-3860 (part of Shanghai) requires EIP-170.
if !evm.chainRules.IsAura || evm.config.HasEip3860(evm.chainRules) {
err = ErrMaxCodeSizeExceeded
}
}
// Reject code starting with 0xEF if EIP-3541 is enabled.
if err == nil && evm.chainRules.IsLondon && len(ret) >= 1 && ret[0] == 0xEF {
err = ErrInvalidCode
}
// if the contract creation ran successfully and no errors were returned
// calculate the gas required to store the code. If the code could not
// be stored due to not enough gas set an error and let it be handled
// by the error checking condition below.
if err == nil {
createDataGas := uint64(len(ret)) * params.CreateDataGas
if contract.UseGas(createDataGas) {
evm.intraBlockState.SetCode(address, ret)
} else if evm.chainRules.IsHomestead {
err = ErrCodeStoreOutOfGas
}
}
// When an error was returned by the EVM or when setting the creation code
// above we revert to the snapshot and consume any gas remaining. Additionally
// when we're in homestead this also counts for code storage gas errors.
if err != nil && (evm.chainRules.IsHomestead || err != ErrCodeStoreOutOfGas) {
evm.intraBlockState.RevertToSnapshot(snapshot)
if err != ErrExecutionReverted {
contract.UseGas(contract.Gas)
}
}
// calculate gasConsumption for deferred captures
gasConsumption = gasRemaining - contract.Gas
return ret, address, contract.Gas, err
}
However, there is no check to ensure the newly created address passed into the function is not one of the precompile contract addresses. The precompile contract addresses are supposed to be reserved only for the precompile contract.
The newly created address is created either via Create() or Create2():
// Create creates a new contract using code as deployment code.
// DESCRIBED: docs/programmers_guide/guide.md#nonce
func (evm *EVM) Create(caller ContractRef, code []byte, gasRemaining uint64, endowment *uint256.Int, bailout bool) (ret []byte, contractAddr libcommon.Address, leftOverGas uint64, err error) {
contractAddr = crypto.CreateAddress(caller.Address(), evm.intraBlockState.GetNonce(caller.Address()))
return evm.create(caller, &codeAndHash{code: code}, gasRemaining, endowment, contractAddr, CREATE, true /* incrementNonce */, bailout)
}
// Create2 creates a new contract using code as deployment code.
//
// The different between Create2 with Create is Create2 uses keccak256(0xff ++ msg.sender ++ salt ++ keccak256(init_code))[12:]
// instead of the usual sender-and-nonce-hash as the address where the contract is initialized at.
// DESCRIBED: docs/programmers_guide/guide.md#nonce
func (evm *EVM) Create2(caller ContractRef, code []byte, gasRemaining uint64, endowment *uint256.Int, salt *uint256.Int, bailout bool) (ret []byte, contractAddr libcommon.Address, leftOverGas uint64, err error) {
codeAndHash := &codeAndHash{code: code}
contractAddr = crypto.CreateAddress2(caller.Address(), salt.Bytes32(), codeAndHash.Hash().Bytes())
return evm.create(caller, codeAndHash, gasRemaining, endowment, contractAddr, CREATE2, true /* incrementNonce */, bailout)
}
Though this check does not seem to be specified in the execution specification and it is unlikely to occur in a short term due to the hardness of hash collision, it is necessary to ensure the logic correctness of the execution. Note that the revm (used in Ethereum client Reth) has such check:
// created address is not allowed to be a precompile.
if self.precompiles.contains(&created_address) {
return return_error(InstructionResult::CreateCollision);
}
Impact Details
In case that the newly created contract address belongs to the precompile contract addresses, the contract will never be functional.
For simplicity, we modify the code inside the Create() to mimic that the newly created address is 0x08 (a precommplile contract address) and test on the Create() .
The test result shows the contract deployment is successful:
=== RUN TestCreateTx
=== PAUSE TestCreateTx
=== CONT TestCreateTx
balance is 10000
Return is , address is 0000000000000000000000000000000000000008, and gas used is 9994
The contract hash is c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470
--- PASS: TestCreateTx (0.04s)
PASS