diff --git a/validation/gas-token_test.go b/validation/gas-token_test.go deleted file mode 100644 index f82522549..000000000 --- a/validation/gas-token_test.go +++ /dev/null @@ -1,90 +0,0 @@ -package validation - -import ( - "context" - "errors" - "testing" - - . "github.com/ethereum-optimism/superchain-registry/superchain" - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/ethclient" - "github.com/stretchr/testify/require" -) - -func testGasToken(t *testing.T, chain *ChainConfig) { - client, err := ethclient.Dial(chain.PublicRPC) - require.NoError(t, err, "Failed to connect to the Ethereum client at RPC url %s", chain.PublicRPC) - defer client.Close() - - // WETH predeploy name() check - want := "0000000000000000000000000000000000000000000000000000000000000020" + // offset - "000000000000000000000000000000000000000000000000000000000000000d" + // length - "5772617070656420457468657200000000000000000000000000000000000000" // "Wrapped Ether" padded to 32 bytes - gotName, err := getHexString("name()", MustHexToAddress("0x4200000000000000000000000000000000000006"), client) - require.NoError(t, err) - require.Equal(t, want, gotName) - - // L1Block .isCustomGasToken() check - got, err := getBool("isCustomGasToken()", MustHexToAddress("0x4200000000000000000000000000000000000015"), client) - - if err != nil { - // Pre: Custom Gas Token feature. Reverting is acceptable. - require.Contains(t, err.Error(), "execution reverted") - } else { - // Post: Custom Gas Token fearure. Must be set to false. - require.False(t, got) - } - - // SystemConfig .isCustomGasToken() check (L1) - client, err = ethclient.Dial(Superchains[chain.Superchain].Config.L1.PublicRPC) - require.NoError(t, err, "Failed to connect to the Ethereum client at RPC url %s", chain.PublicRPC) - defer client.Close() - - got, err = getBool("isCustomGasToken()", Addresses[chain.ChainID].SystemConfigProxy, client) - if err != nil { - // Pre: Custom Gas Token feature. Reverting is acceptable. - require.Contains(t, err.Error(), "execution reverted") - } else { - // Post: Custom Gas Token fearure. Must be set to false. - require.False(t, got) - } -} - -func getBytes(method string, contractAddress Address, client *ethclient.Client) ([]byte, error) { - addr := (common.Address(contractAddress)) - callMsg := ethereum.CallMsg{ - To: &addr, - Data: crypto.Keccak256([]byte(method))[:4], - } - - // Make the call - callContract := func(msg ethereum.CallMsg) ([]byte, error) { - return client.CallContract(context.Background(), msg, nil) - } - - return Retry(callContract)(callMsg) -} - -func getHexString(method string, contractAddress Address, client *ethclient.Client) (string, error) { - result, err := getBytes(method, contractAddress, client) - - return common.Bytes2Hex(result), err -} - -func getBool(method string, contractAddress Address, client *ethclient.Client) (bool, error) { - result, err := getBytes(method, contractAddress, client) - if err != nil { - return false, err - } - - switch common.HexToHash(string(result)) { - case common.Hash{1}: - return true, nil - case common.Hash{}: - return false, nil - default: - return false, errors.New("unexpected non-bool return value") - } -} diff --git a/validation/gas_token.go b/validation/gas_token.go new file mode 100644 index 000000000..6bf52c632 --- /dev/null +++ b/validation/gas_token.go @@ -0,0 +1,102 @@ +package validation + +import ( + "context" + "errors" + "fmt" + "strings" + "testing" + + "github.com/ethereum-optimism/superchain-registry/superchain" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/stretchr/testify/require" +) + +func testGasToken(t *testing.T, chain *superchain.ChainConfig) { + l1Client, err := ethclient.Dial(superchain.Superchains[chain.Superchain].Config.L1.PublicRPC) + require.NoError(t, err, "Failed to connect to L1 EthClient at RPC url %s", superchain.Superchains[chain.Superchain].Config.L1.PublicRPC) + defer l1Client.Close() + + l2Client, err := ethclient.Dial(chain.PublicRPC) + require.NoError(t, err, "Failed to connect to L2 EthClient at RPC url %s", chain.PublicRPC) + defer l2Client.Close() + + err = CheckGasToken(chain, l1Client, l2Client) + require.NoError(t, err) +} + +func CheckGasToken(chain *superchain.ChainConfig, l1Client EthClient, l2Client EthClient) error { + weth9PredeployAddress := superchain.MustHexToAddress("0x4200000000000000000000000000000000000006") + want := "0000000000000000000000000000000000000000000000000000000000000020" + // offset + "000000000000000000000000000000000000000000000000000000000000000d" + // length + "5772617070656420457468657200000000000000000000000000000000000000" // "Wrapped Ether" padded to 32 bytes + gotName, err := getHexString("name()", weth9PredeployAddress, l2Client) + if err != nil { + return err + } + if want != gotName { + return fmt.Errorf("predeploy WETH9.name(): want=%s, got=%s", want, gotName) + } + + l1BlockPredeployAddress := superchain.MustHexToAddress("0x4200000000000000000000000000000000000015") + isCustomGasToken, err := getBool("isCustomGasToken()", l1BlockPredeployAddress, l2Client) + if err != nil && !strings.Contains(err.Error(), "execution reverted") { + // Pre: reverting is acceptable + return err + } else { + // Post: must be set to false + if isCustomGasToken { + return fmt.Errorf("L1Block.isCustomGasToken() must return false") + } + } + + isCustomGasToken, err = getBool("isCustomGasToken()", superchain.Addresses[chain.ChainID].SystemConfigProxy, l1Client) + if err != nil && !strings.Contains(err.Error(), "execution reverted") { + // Pre: reverting is acceptable + return err + } else { + // Post: must be set to false + if isCustomGasToken { + return fmt.Errorf("SystemConfigProxy.isCustomGasToken() must return false") + } + } + return nil +} + +func getBytes(method string, contractAddress superchain.Address, client EthClient) ([]byte, error) { + addr := (common.Address(contractAddress)) + callMsg := ethereum.CallMsg{ + To: &addr, + Data: crypto.Keccak256([]byte(method))[:4], + } + + callContract := func(msg ethereum.CallMsg) ([]byte, error) { + return client.CallContract(context.Background(), msg, nil) + } + + return Retry(callContract)(callMsg) +} + +func getHexString(method string, contractAddress superchain.Address, client EthClient) (string, error) { + result, err := getBytes(method, contractAddress, client) + return common.Bytes2Hex(result), err +} + +func getBool(method string, contractAddress superchain.Address, client EthClient) (bool, error) { + result, err := getBytes(method, contractAddress, client) + if err != nil { + return false, err + } + + switch common.HexToHash(string(result)) { + case common.Hash{1}: + return true, nil + case common.Hash{}: + return false, nil + default: + return false, errors.New("unexpected non-bool return value") + } +} diff --git a/validation/gas_token_test.go b/validation/gas_token_test.go new file mode 100644 index 000000000..74a895950 --- /dev/null +++ b/validation/gas_token_test.go @@ -0,0 +1,120 @@ +package validation + +import ( + "errors" + "math/big" + "testing" + + "github.com/ethereum-optimism/superchain-registry/superchain" + "github.com/ethereum-optimism/superchain-registry/validation/testutils" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestGetBytes_Success(t *testing.T) { + t.Parallel() + + mockClient := &testutils.MockEthClient{} + expected := []byte{0xde, 0xad, 0xbe, 0xef} + mockClient.On("CallContract", mock.Anything, mock.Anything, (*big.Int)(nil)). + Return(expected, nil). + Once() + + result, err := getBytes("myMethod()", superchain.Address{}, mockClient) + require.NoError(t, err) + require.Equal(t, expected, result) + + mockClient.AssertExpectations(t) +} + +func TestGetBytes_Error(t *testing.T) { + t.Parallel() + + mockClient := &testutils.MockEthClient{} + mockClient.On("CallContract", mock.Anything, mock.Anything, (*big.Int)(nil)). + Return([]byte{}, errors.New("some call error")). + Times(DefaultMaxRetries) + + _, err := getBytes("failingMethod()", superchain.Address{}, mockClient) + require.Error(t, err) + require.Contains(t, err.Error(), "some call error") + + mockClient.AssertExpectations(t) +} + +func TestGetHexString_Success(t *testing.T) { + t.Parallel() + + mockClient := &testutils.MockEthClient{} + expected := []byte{0x00, 0x11, 0x22, 0x33} + mockClient.On("CallContract", mock.Anything, mock.Anything, (*big.Int)(nil)). + Return(expected, nil). + Once() + + hexVal, err := getHexString("hexMethod()", superchain.Address{}, mockClient) + require.NoError(t, err) + require.Equal(t, "00112233", hexVal) + + mockClient.AssertExpectations(t) +} + +func TestGetHexString_Error(t *testing.T) { + t.Parallel() + + mockClient := &testutils.MockEthClient{} + mockClient.On("CallContract", mock.Anything, mock.Anything, (*big.Int)(nil)). + Return([]byte{}, errors.New("getHexString error")). + Times(DefaultMaxRetries) + + _, err := getHexString("hexMethod()", superchain.Address{}, mockClient) + require.Error(t, err) + require.Contains(t, err.Error(), "getHexString error") + + mockClient.AssertExpectations(t) +} + +func TestGetBool_True(t *testing.T) { + t.Parallel() + + // Returning bytes that correspond to `true` value + mockClient := &testutils.MockEthClient{} + mockClient.On("CallContract", mock.Anything, mock.Anything, (*big.Int)(nil)). + Return([]byte("0x0100000000000000000000000000000000000000000000000000000000000000"), nil). + Once() + + val, err := getBool("boolMethod()", superchain.Address{}, mockClient) + require.NoError(t, err) + require.True(t, val) + + mockClient.AssertExpectations(t) +} + +func TestGetBool_False(t *testing.T) { + t.Parallel() + + mockClient := &testutils.MockEthClient{} + mockClient.On("CallContract", mock.Anything, mock.Anything, (*big.Int)(nil)). + Return([]byte(""), nil). + Once() + + val, err := getBool("boolMethod()", superchain.Address{}, mockClient) + require.NoError(t, err) + require.False(t, val) + + mockClient.AssertExpectations(t) +} + +func TestGetBool_ErrorUnexpectedValue(t *testing.T) { + t.Parallel() + + mockClient := &testutils.MockEthClient{} + mockClient.On("CallContract", mock.Anything, mock.Anything, (*big.Int)(nil)). + Return([]byte("0xabcdef"), nil). + Once() + + _, err := getBool("boolMethod()", superchain.Address{}, mockClient) + require.Error(t, err) + require.Contains(t, err.Error(), "unexpected non-bool return value") + + mockClient.AssertExpectations(t) +} diff --git a/validation/go.mod b/validation/go.mod index a667a9705..c6e10ea2b 100644 --- a/validation/go.mod +++ b/validation/go.mod @@ -76,6 +76,7 @@ require ( github.com/rs/cors v1.11.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/supranational/blst v0.3.13 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect diff --git a/validation/testutils/mock_ethclient.go b/validation/testutils/mock_ethclient.go new file mode 100644 index 000000000..8516bee4d --- /dev/null +++ b/validation/testutils/mock_ethclient.go @@ -0,0 +1,18 @@ +package testutils + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum" + "github.com/stretchr/testify/mock" +) + +type MockEthClient struct { + mock.Mock +} + +func (m *MockEthClient) CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { + args := m.Called(ctx, msg, blockNumber) + return args.Get(0).([]byte), args.Error(1) +} diff --git a/validation/utils.go b/validation/utils.go index 6a9d0feee..87e0bb36d 100644 --- a/validation/utils.go +++ b/validation/utils.go @@ -11,9 +11,14 @@ import ( "github.com/ethereum-optimism/optimism/op-service/retry" "github.com/ethereum-optimism/superchain-registry/superchain" + "github.com/ethereum/go-ethereum" "github.com/stretchr/testify/assert" ) +type EthClient interface { + CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) +} + // isBigIntWithinBounds returns true if actual is within bounds, where the bounds are [lower bound, upper bound] and are inclusive. var isBigIntWithinBounds = func(actual *big.Int, bounds [2]*big.Int) bool { if (bounds[1].Cmp(bounds[0])) < 0 {