unionlabs/union · error

Expected selector 0x

Error message

Expected selector 0x{:08x}, got 0x{:08x} (raw: {})

What it means

The final assertion failure of send_and_expect_revert: a selector-shaped hex was extracted and its first 4 bytes parsed as u32, but actual != expected_revert_code. The tx reverted with a different error than the test asserted. The message helpfully prints both selectors and the full raw hex so you can identify which contract error actually fired.

Solutions

  1. Decode the actual selector in the message against the contract's error list (e.g. cast 4byte / selector tables) to learn which error fired, then fix the precondition so the intended branch trips.
  2. Recompute expected_revert_code from the current ABI: keccak256("ErrorName(type1,type2)")[0..4].
  3. If the actual error reveals a test-fixture problem (missing funds, channel not set up), fix the setup, not the constant.
  4. Prefer deriving the expected selector in code from the typed contract bindings rather than a hand-written u32.

Example fix

// before — hand-written selector for the old ABI
const EXPECTED: u32 = 0x9e87c1a2;

// after — derive it from current bindings
use contract::Error;
let expected = u32::from_be_bytes(Error::Unauthorized.selector());
Defensive patterns

Strategy: validation

Validate before calling

// assert intent before the call: derive expected from live bindings
let expected = u32::from_be_bytes(MyError::InsufficientFunds.selector());
assert_eq!(expected, EXPECTED_REVERT_CODE, "fixture selector drifted from ABI");

Try / catch

match tw.send_and_expect_revert(&src, contract, msg, expected, &signer).await {
    Err(e) if e.to_string().contains("Expected selector") => { /* decode actual selector, fix precondition */ panic!("{e:#}") }
    other => other?,
}

Prevention

When it happens

Trigger: The msg hits a different require/error branch than intended (e.g. INSUFFICIENT_FUNDS instead of UNAUTHORIZED because fixture state differs), expected_revert_code computed from the wrong error signature or a stale ABI, or big-endian/endianness or digit-order mistake when hand-writing the expected constant.

Common situations: Contract error enum reordered or renamed after an upgrade while the test constant stayed; shared chain state changed which guard trips first; expected selector written with padded wrong width (0x{:08x} formatting hides missing digits).

Related errors


AI-assisted analysis of unionlabs/union@031785bb6d (2026-08-16). Data as JSON: /api/errors/137ec2e883151173. Report an issue: GitHub.

Appendix: source

Thrown at tools/union-test/src/lib.rs:1061

                let hex_full = caps
                    .get(1)
                    .map(|m| m.as_str())
                    .ok_or_else(|| anyhow!("regex matched but capture group missing"))?;

                let hexdigits = hex_full.trim_start_matches("0x");
                if hexdigits.len() < 8 {
                    anyhow::bail!("rawValue too short for a selector: {}", hex_full);
                }
                let selector_hex = &hexdigits[..8]; // first 4 bytes

                let actual = u32::from_str_radix(selector_hex, 16).with_context(|| {
                    format!("parsing revert selector `{selector_hex}` from `{hex_full}`")
                })?;

                if actual == expected_revert_code {
                    Ok(())
                } else {
                    anyhow::bail!(
                        "Expected selector 0x{:08x}, got 0x{:08x} (raw: {})",
                        expected_revert_code,
                        actual,
                        hex_full
                    )
                }
            }
        }
    }

    pub async fn send_and_recv_withdraw<Src: ChainEndpoint, Dst: ChainEndpoint>(
        &self,
        source_chain: &Src,
        contract: Src::Contract,
        msg: Src::Msg,
        destination_chain: &Dst,
        timeout: Duration,
        signer: Src::ProviderType,

View on GitHub (pinned to 031785bb6d)