diff --git a/harnesses/bridge-monitor/cmd/monitor/onchain_balance.go b/harnesses/bridge-monitor/cmd/monitor/onchain_balance.go index c6aac96d1..60a42324a 100644 --- a/harnesses/bridge-monitor/cmd/monitor/onchain_balance.go +++ b/harnesses/bridge-monitor/cmd/monitor/onchain_balance.go @@ -40,11 +40,25 @@ func (tx *TxExecutor) erc20BalanceOf(chain string, token, owner common.Address) } // evmClientFor maps a Route chain name (Base / Arbitrum) to the cached ethclient. -func (tx *TxExecutor) evmClientFor(chain string) interface{ CallContract(context.Context, ethereum.CallMsg, *big.Int) ([]byte, error) } { +func (tx *TxExecutor) evmClientFor(chain string) interface { + CallContract(context.Context, ethereum.CallMsg, *big.Int) ([]byte, error) +} { + // Return a genuinely-nil interface when the concrete client failed to + // initialize. Returning a typed nil *ethclient.Client here would satisfy + // `client == nil` as FALSE in erc20BalanceOf (non-nil interface wrapping a + // nil pointer), then panic on CallContract. This is what crash-looped the + // VPS container when the portfolio API went down and the on-chain fallback + // ran against un-dialled EVM clients. switch strings.ToLower(chain) { case "base": + if tx.baseClient == nil { + return nil + } return tx.baseClient case "arbitrum": + if tx.arbitrumClient == nil { + return nil + } return tx.arbitrumClient } return nil diff --git a/harnesses/bridge-monitor/cmd/monitor/onchain_balance_test.go b/harnesses/bridge-monitor/cmd/monitor/onchain_balance_test.go new file mode 100644 index 000000000..43f3d74dd --- /dev/null +++ b/harnesses/bridge-monitor/cmd/monitor/onchain_balance_test.go @@ -0,0 +1,19 @@ +package main + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" +) + +// TestErc20BalanceNilClientNoPanic guards the nil-interface trap that crash- +// looped the VPS: with un-dialled EVM clients, erc20BalanceOf must return an +// error, never panic on CallContract. +func TestErc20BalanceNilClientNoPanic(t *testing.T) { + tx := &TxExecutor{} // baseClient / arbitrumClient are nil + for _, chain := range []string{"base", "arbitrum"} { + if _, err := tx.erc20BalanceOf(chain, common.Address{}, common.Address{}); err == nil { + t.Errorf("%s: expected error for nil client, got nil (would have panicked)", chain) + } + } +}