Submitted on Nov 28th 2024 at 17:16:42 UTC by @CertiK for
Report ID: #37199
Report Type: Blockchain/DLT
Report severity: Low
Target: https://github.com/ledgerwatch/erigon
Impacts:
Unintended chain split affecting less than 25% of the network (Network partition)
Description
Brief/Intro
A potential chain fork has been discovered in the Ethereum client Erigon ( https://github.com/erigontech/erigon ) due to the shallow copy of the byte slice in precompile contract dataCopy.
Vulnerability Details
The issue outlined in this report pertains to the precompile contract dataCopy, detailed as follows:
// data copy implemented as a native contract.
type dataCopy struct{}
// RequiredGas returns the gas required to execute the pre-compiled contract.
//
// This method does not require any overflow checking as the input size gas costs
// required for anything significant is so high it's impossible to pay for.
func (c *dataCopy) RequiredGas(input []byte) uint64 {
return uint64(len(input)+31)/32*params.IdentityPerWordGas + params.IdentityBaseGas
}
func (c *dataCopy) Run(in []byte) ([]byte, error) {
return in, nil
}
Which directly returns the input as a shallow copy of the input, which does not align with other Ethereum clients, for example, in Go Ethereum:
This discrepancy in implementation could lead to chain fork as observed in go-ethereum clients in the post mortem: https://gist.github.com/karalabe/e1891c8a99fdc16c4e60d9713c35401f
Impact Details
This discrepancy in implementation of shallow copy and deep copy could lead to chain fork.
This attack scenario has been observed in two opcodes RETURNDATASIZE and RETURNDATACOPY in go-ethereum as described in the post mortem: https://gist.github.com/karalabe/e1891c8a99fdc16c4e60d9713c35401f
Here we provide a unit test to show the difference between the implementation of data copy precompile contract with shallow copy and deep copy:
package vm
import (
"bytes"
"encoding/json"
"fmt"
"os"
"testing"
"time"
libcommon "github.com/ledgerwatch/erigon-lib/common"
"github.com/ledgerwatch/erigon/common"
)
func CopyBytes(b []byte) (copiedBytes []byte) {
if b == nil {
return nil
}
copiedBytes = make([]byte, len(b))
copy(copiedBytes, b)
return
}
func TestDatacopyPrecompile(t *testing.T) {
dataCopyContract := PrecompiledContractsCancun[libcommon.BytesToAddress([]byte{0x04})]
input1 := []byte{0x01, 0x02, 0x03, 0x04}
maxGas := uint64(10000)
//////////////shallow copy/////////
output1, suppliedGas, err := RunPrecompiledContract(dataCopyContract, input1, maxGas)
if err == nil {
fmt.Printf("Output is: %x, supplied gas is: %d\n", output1, suppliedGas)
}
output1[0] = 0xff
fmt.Println("//////////////shallow copy/////////")
fmt.Printf("Input is changed from %x, to: %x\n", input1, output1)
fmt.Printf("Output is: %x\n", output1)
//////////////deep copy/////////
input2 := []byte{0x01, 0x02, 0x03, 0x04}
output2 := CopyBytes(input2)
output2[0] = 0xff
fmt.Println("//////////////deep copy/////////")
fmt.Printf("Input is changed from %x, to: %x\n", input2, output2)
fmt.Printf("Output is: %x\n", output2)
}
Test result:
=== RUN TestDatacopyPrecompile
Output is: 01020304, supplied gas is: 9982
//////////////shallow copy/////////
Input is changed from ff020304, to: ff020304
Output is: ff020304
//////////////deep copy/////////
Input is changed from 01020304, to: ff020304
Output is: ff020304
--- PASS: TestDatacopyPrecompile (0.00s)
PASS
The result shows that the shallow copy modifies the original input while deep copy does not.
Since Erigon uses the shallow copy in the data copy precompile contract, once the input is modified, it would lead to inconsistent data with other Ethereum clients, potentially lead to chain fork/split.